You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +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();
|
httpBackend.verifyNoOutstandingExpectation();
|
||||||
client.stopClient();
|
client.stopClient();
|
||||||
Thread.setServerSideSupport(FeatureSupport.None);
|
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() {
|
describe("getEventTimeline", function() {
|
||||||
it("should create a new timeline for new events", function() {
|
it("should create a new timeline for new events", function() {
|
||||||
const room = client.getRoom(roomId)!;
|
const room = client.getRoom(roomId)!;
|
||||||
@@ -595,22 +601,8 @@ describe("MatrixClient event timelines", function() {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
client.clientOpts.experimentalThreadSupport = true;
|
client.clientOpts.experimentalThreadSupport = true;
|
||||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
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 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!))
|
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||||
.respond(200, function() {
|
.respond(200, function() {
|
||||||
@@ -619,7 +611,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
|
||||||
.respond(200, function() {
|
.respond(200, function() {
|
||||||
return {
|
return {
|
||||||
original_event: THREAD_ROOT,
|
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();
|
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;
|
const timeline = await timelinePromise;
|
||||||
|
|
||||||
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
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() {
|
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";
|
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
|
||||||
|
|
||||||
function respondToFilter(): ExpectedHttpRequest {
|
function respondToFilter(): ExpectedHttpRequest {
|
||||||
@@ -1050,7 +1074,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
next_batch: RANDOM_TOKEN as string | null,
|
next_batch: RANDOM_TOKEN as string | null,
|
||||||
},
|
},
|
||||||
): ExpectedHttpRequest {
|
): 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,
|
$roomId: roomId,
|
||||||
}));
|
}));
|
||||||
request.respond(200, response);
|
request.respond(200, response);
|
||||||
@@ -1089,8 +1113,9 @@ describe("MatrixClient event timelines", function() {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
client.clientOpts.experimentalThreadSupport = true;
|
client.clientOpts.experimentalThreadSupport = true;
|
||||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||||
|
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testPagination(timelineSet: EventTimelineSet, direction: Direction) {
|
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() {
|
it("should allow you to paginate all threads backwards", async function() {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
const timelineSets = await (room?.createThreadsTimelineSets());
|
const timelineSets = await room!.createThreadsTimelineSets();
|
||||||
expect(timelineSets).not.toBeNull();
|
expect(timelineSets).not.toBeNull();
|
||||||
const [allThreads, myThreads] = timelineSets!;
|
const [allThreads, myThreads] = timelineSets!;
|
||||||
await testPagination(allThreads, Direction.Backward);
|
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() {
|
it("should allow you to paginate all threads forwards", async function() {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
const timelineSets = await (room?.createThreadsTimelineSets());
|
const timelineSets = await room!.createThreadsTimelineSets();
|
||||||
expect(timelineSets).not.toBeNull();
|
expect(timelineSets).not.toBeNull();
|
||||||
const [allThreads, myThreads] = timelineSets!;
|
const [allThreads, myThreads] = timelineSets!;
|
||||||
|
|
||||||
@@ -1130,7 +1155,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
it("should allow fetching all threads", async function() {
|
it("should allow fetching all threads", async function() {
|
||||||
const room = client.getRoom(roomId)!;
|
const room = client.getRoom(roomId)!;
|
||||||
const timelineSets = await room.createThreadsTimelineSets();
|
const timelineSets = await room!.createThreadsTimelineSets();
|
||||||
expect(timelineSets).not.toBeNull();
|
expect(timelineSets).not.toBeNull();
|
||||||
respondToThreads();
|
respondToThreads();
|
||||||
respondToThreads();
|
respondToThreads();
|
||||||
@@ -1418,11 +1443,8 @@ describe("MatrixClient event timelines", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should re-insert room IDs for bundled thread relation events", async () => {
|
describe("should re-insert room IDs for bundled thread relation events", () => {
|
||||||
// @ts-ignore
|
async function doTest() {
|
||||||
client.clientOpts.experimentalThreadSupport = true;
|
|
||||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
|
||||||
|
|
||||||
httpBackend.when("GET", "/sync").respond(200, {
|
httpBackend.when("GET", "/sync").respond(200, {
|
||||||
next_batch: "s_5_4",
|
next_batch: "s_5_4",
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -1444,6 +1466,14 @@ describe("MatrixClient event timelines", function() {
|
|||||||
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
||||||
const timelineSet = thread.timelineSet;
|
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!))
|
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||||
.respond(200, {
|
.respond(200, {
|
||||||
start: "start_token",
|
start: "start_token",
|
||||||
@@ -1455,18 +1485,23 @@ describe("MatrixClient event timelines", function() {
|
|||||||
});
|
});
|
||||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
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() {
|
.respond(200, function() {
|
||||||
return {
|
return {
|
||||||
original_event: THREAD_ROOT,
|
original_event: THREAD_ROOT,
|
||||||
chunk: [THREAD_REPLY],
|
chunk: [THREAD_REPLY],
|
||||||
// no next batch as this is the oldest end of the timeline
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await Promise.all([
|
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
|
||||||
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!),
|
|
||||||
httpBackend.flushAllExpected(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
httpBackend.when("GET", "/sync").respond(200, {
|
httpBackend.when("GET", "/sync").respond(200, {
|
||||||
next_batch: "s_5_5",
|
next_batch: "s_5_5",
|
||||||
@@ -1486,6 +1521,37 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||||
|
|
||||||
expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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();
|
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 () => {
|
it("should read related events with relation type", async () => {
|
||||||
@@ -72,7 +72,7 @@ describe("MatrixClient relations", () => {
|
|||||||
|
|
||||||
await httpBackend!.flushAllExpected();
|
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 () => {
|
it("should read related events with relation type and event type", async () => {
|
||||||
@@ -87,7 +87,7 @@ describe("MatrixClient relations", () => {
|
|||||||
|
|
||||||
await httpBackend!.flushAllExpected();
|
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 () => {
|
it("should read related events with custom options", async () => {
|
||||||
@@ -107,7 +107,7 @@ describe("MatrixClient relations", () => {
|
|||||||
|
|
||||||
await httpBackend!.flushAllExpected();
|
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 () => {
|
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||||
|
@@ -231,6 +231,130 @@ describe("MatrixClient", function() {
|
|||||||
client.stopClient();
|
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 () => {
|
it("should create (unstable) file trees", async () => {
|
||||||
const userId = "@test:example.org";
|
const userId = "@test:example.org";
|
||||||
const roomId = "!room: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", () => {
|
describe("redactEvent", () => {
|
||||||
const roomId = "!room:example.org";
|
const roomId = "!room:example.org";
|
||||||
const mockRoom = {
|
const mockRoom = {
|
||||||
|
@@ -39,7 +39,7 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
|||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { emitPromise } from "../test-utils/test-utils";
|
import { emitPromise } from "../test-utils/test-utils";
|
||||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
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 { WrappedReceipt } from "../../src/models/read-receipt";
|
||||||
import { Crypto } from "../../src/crypto";
|
import { Crypto } from "../../src/crypto";
|
||||||
|
|
||||||
@@ -2203,6 +2203,7 @@ describe("Room", function() {
|
|||||||
|
|
||||||
it("Edits update the lastReply event", async () => {
|
it("Edits update the lastReply event", async () => {
|
||||||
room.client.supportsExperimentalThreads = () => true;
|
room.client.supportsExperimentalThreads = () => true;
|
||||||
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||||
|
|
||||||
const randomMessage = mkMessage();
|
const randomMessage = mkMessage();
|
||||||
const threadRoot = mkMessage();
|
const threadRoot = mkMessage();
|
||||||
@@ -2216,7 +2217,7 @@ describe("Room", function() {
|
|||||||
unsigned: {
|
unsigned: {
|
||||||
"age": 123,
|
"age": 123,
|
||||||
"m.relations": {
|
"m.relations": {
|
||||||
"m.thread": {
|
[THREAD_RELATION_TYPE.name]: {
|
||||||
latest_event: threadResponse.event,
|
latest_event: threadResponse.event,
|
||||||
count: 2,
|
count: 2,
|
||||||
current_user_participated: true,
|
current_user_participated: true,
|
||||||
@@ -2228,11 +2229,29 @@ describe("Room", function() {
|
|||||||
let prom = emitPromise(room, ThreadEvent.New);
|
let prom = emitPromise(room, ThreadEvent.New);
|
||||||
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
||||||
const thread = await prom;
|
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);
|
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]);
|
room.addLiveEvents([threadResponseEdit]);
|
||||||
await prom;
|
await prom;
|
||||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
|
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 () => {
|
it("Redactions to thread responses decrement the length", async () => {
|
||||||
room.client.supportsExperimentalThreads = () => true;
|
room.client.supportsExperimentalThreads = () => true;
|
||||||
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||||
|
|
||||||
const threadRoot = mkMessage();
|
const threadRoot = mkMessage();
|
||||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||||
@@ -2252,7 +2272,7 @@ describe("Room", function() {
|
|||||||
unsigned: {
|
unsigned: {
|
||||||
"age": 123,
|
"age": 123,
|
||||||
"m.relations": {
|
"m.relations": {
|
||||||
"m.thread": {
|
[THREAD_RELATION_TYPE.name]: {
|
||||||
latest_event: threadResponse2.event,
|
latest_event: threadResponse2.event,
|
||||||
count: 2,
|
count: 2,
|
||||||
current_user_participated: true,
|
current_user_participated: true,
|
||||||
@@ -2264,10 +2284,36 @@ describe("Room", function() {
|
|||||||
let prom = emitPromise(room, ThreadEvent.New);
|
let prom = emitPromise(room, ThreadEvent.New);
|
||||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||||
const thread = await prom;
|
const thread = await prom;
|
||||||
|
await emitPromise(room, ThreadEvent.Update);
|
||||||
|
|
||||||
expect(thread).toHaveLength(2);
|
expect(thread).toHaveLength(2);
|
||||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
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);
|
prom = emitPromise(thread, ThreadEvent.Update);
|
||||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||||
room.addLiveEvents([threadResponse1Redaction]);
|
room.addLiveEvents([threadResponse1Redaction]);
|
||||||
@@ -2278,6 +2324,7 @@ describe("Room", function() {
|
|||||||
|
|
||||||
it("Redactions to reactions in threads do not decrement the length", async () => {
|
it("Redactions to reactions in threads do not decrement the length", async () => {
|
||||||
room.client.supportsExperimentalThreads = () => true;
|
room.client.supportsExperimentalThreads = () => true;
|
||||||
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||||
|
|
||||||
const threadRoot = mkMessage();
|
const threadRoot = mkMessage();
|
||||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||||
@@ -2291,7 +2338,7 @@ describe("Room", function() {
|
|||||||
unsigned: {
|
unsigned: {
|
||||||
"age": 123,
|
"age": 123,
|
||||||
"m.relations": {
|
"m.relations": {
|
||||||
"m.thread": {
|
[THREAD_RELATION_TYPE.name]: {
|
||||||
latest_event: threadResponse2.event,
|
latest_event: threadResponse2.event,
|
||||||
count: 2,
|
count: 2,
|
||||||
current_user_participated: true,
|
current_user_participated: true,
|
||||||
@@ -2303,6 +2350,7 @@ describe("Room", function() {
|
|||||||
const prom = emitPromise(room, ThreadEvent.New);
|
const prom = emitPromise(room, ThreadEvent.New);
|
||||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||||
const thread = await prom;
|
const thread = await prom;
|
||||||
|
await emitPromise(room, ThreadEvent.Update);
|
||||||
|
|
||||||
expect(thread).toHaveLength(2);
|
expect(thread).toHaveLength(2);
|
||||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
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 () => {
|
it("should not decrement the length when the thread root is redacted", async () => {
|
||||||
room.client.supportsExperimentalThreads = () => true;
|
room.client.supportsExperimentalThreads = () => true;
|
||||||
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||||
|
|
||||||
const threadRoot = mkMessage();
|
const threadRoot = mkMessage();
|
||||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||||
@@ -2328,7 +2377,7 @@ describe("Room", function() {
|
|||||||
unsigned: {
|
unsigned: {
|
||||||
"age": 123,
|
"age": 123,
|
||||||
"m.relations": {
|
"m.relations": {
|
||||||
"m.thread": {
|
[THREAD_RELATION_TYPE.name]: {
|
||||||
latest_event: threadResponse2.event,
|
latest_event: threadResponse2.event,
|
||||||
count: 2,
|
count: 2,
|
||||||
current_user_participated: true,
|
current_user_participated: true,
|
||||||
@@ -2340,6 +2389,7 @@ describe("Room", function() {
|
|||||||
let prom = emitPromise(room, ThreadEvent.New);
|
let prom = emitPromise(room, ThreadEvent.New);
|
||||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||||
const thread = await prom;
|
const thread = await prom;
|
||||||
|
await emitPromise(room, ThreadEvent.Update);
|
||||||
|
|
||||||
expect(thread).toHaveLength(2);
|
expect(thread).toHaveLength(2);
|
||||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||||
@@ -2353,6 +2403,18 @@ describe("Room", function() {
|
|||||||
|
|
||||||
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
||||||
room.client.supportsExperimentalThreads = () => true;
|
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 threadRoot = mkMessage();
|
||||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||||
@@ -2377,21 +2439,53 @@ describe("Room", function() {
|
|||||||
let prom = emitPromise(room, ThreadEvent.New);
|
let prom = emitPromise(room, ThreadEvent.New);
|
||||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||||
const thread = await prom;
|
const thread = await prom;
|
||||||
|
await emitPromise(room, ThreadEvent.Update);
|
||||||
|
|
||||||
expect(thread).toHaveLength(2);
|
expect(thread).toHaveLength(2);
|
||||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
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);
|
prom = emitPromise(room, ThreadEvent.Update);
|
||||||
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
||||||
room.addLiveEvents([threadResponse2Redaction]);
|
room.addLiveEvents([threadResponse2Redaction]);
|
||||||
await prom;
|
await prom;
|
||||||
|
await emitPromise(room, ThreadEvent.Update);
|
||||||
expect(thread).toHaveLength(1);
|
expect(thread).toHaveLength(1);
|
||||||
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
|
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);
|
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||||
room.addLiveEvents([threadResponse1Redaction]);
|
room.addLiveEvents([threadResponse1Redaction]);
|
||||||
await prom;
|
await prom;
|
||||||
|
await prom2;
|
||||||
expect(thread).toHaveLength(0);
|
expect(thread).toHaveLength(0);
|
||||||
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
|
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
|
||||||
});
|
});
|
||||||
@@ -2400,6 +2494,7 @@ describe("Room", function() {
|
|||||||
describe("eventShouldLiveIn", () => {
|
describe("eventShouldLiveIn", () => {
|
||||||
const client = new TestClient(userA).client;
|
const client = new TestClient(userA).client;
|
||||||
client.supportsExperimentalThreads = () => true;
|
client.supportsExperimentalThreads = () => true;
|
||||||
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||||
const room = new Room(roomId, client, userA);
|
const room = new Room(roomId, client, userA);
|
||||||
|
|
||||||
it("thread root and its relations&redactions should be in both", () => {
|
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 { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
|
||||||
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
|
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import { QueryDict, sleep } from './utils';
|
import { replaceParam, QueryDict, sleep } from './utils';
|
||||||
import { Direction, EventTimeline } from "./models/event-timeline";
|
import { Direction, EventTimeline } from "./models/event-timeline";
|
||||||
import { IActionsObject, PushProcessor } from "./pushprocessor";
|
import { IActionsObject, PushProcessor } from "./pushprocessor";
|
||||||
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
||||||
@@ -193,7 +193,14 @@ import { TypedEventEmitter } from "./models/typed-event-emitter";
|
|||||||
import { ReceiptType } from "./@types/read_receipts";
|
import { ReceiptType } from "./@types/read_receipts";
|
||||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
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 { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||||
import { UnstableValue } from "./NamespacedValue";
|
import { UnstableValue } from "./NamespacedValue";
|
||||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||||
@@ -1192,9 +1199,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
||||||
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
||||||
|
|
||||||
const { threads, list } = await this.doesServerSupportThread();
|
const { threads, list, fwdPagination } = await this.doesServerSupportThread();
|
||||||
Thread.setServerSideSupport(threads);
|
Thread.setServerSideSupport(threads);
|
||||||
Thread.setServerSideListSupport(list);
|
Thread.setServerSideListSupport(list);
|
||||||
|
Thread.setServerSideFwdPaginationSupport(fwdPagination);
|
||||||
|
|
||||||
// shallow-copy the opts dict before modifying and storing it
|
// shallow-copy the opts dict before modifying and storing it
|
||||||
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
||||||
@@ -5171,6 +5179,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return timelineSet.getTimelineForEvent(eventId);
|
return timelineSet.getTimelineForEvent(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timelineSet.thread && this.supportsExperimentalThreads()) {
|
||||||
|
return this.getThreadTimeline(timelineSet, eventId);
|
||||||
|
}
|
||||||
|
|
||||||
const path = utils.encodeUri(
|
const path = utils.encodeUri(
|
||||||
"/rooms/$roomId/context/$eventId", {
|
"/rooms/$roomId/context/$eventId", {
|
||||||
$roomId: timelineSet.room.roomId,
|
$roomId: timelineSet.room.roomId,
|
||||||
@@ -5196,6 +5208,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
const mapper = this.getEventMapper();
|
const mapper = this.getEventMapper();
|
||||||
const event = mapper(res.event);
|
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 = [
|
const events = [
|
||||||
// Order events from most recent to oldest (reverse-chronological).
|
// 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.
|
// 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),
|
...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.
|
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
|
||||||
let timeline = timelineSet.getTimelineForEvent(events[0].getId()!);
|
let timeline = timelineSet.getTimelineForEvent(events[0].getId()!);
|
||||||
if (timeline) {
|
if (timeline) {
|
||||||
@@ -5261,6 +5245,154 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
?? timeline;
|
?? 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
|
* 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
|
* 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");
|
throw new Error("getLatestTimeline only supports room timelines");
|
||||||
}
|
}
|
||||||
|
|
||||||
let res: IMessagesResponse;
|
let event;
|
||||||
const roomId = timelineSet.room.roomId;
|
if (timelineSet.threadListType !== null) {
|
||||||
if (timelineSet.isThreadTimeline) {
|
const res = await this.createThreadListMessagesRequest(
|
||||||
res = await this.createThreadListMessagesRequest(
|
timelineSet.room.roomId,
|
||||||
roomId,
|
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
Direction.Backward,
|
Direction.Backward,
|
||||||
|
timelineSet.threadListType,
|
||||||
timelineSet.getFilter(),
|
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 {
|
} else {
|
||||||
res = await this.createMessagesRequest(
|
const messagesPath = utils.encodeUri(
|
||||||
roomId,
|
"/rooms/$roomId/messages", {
|
||||||
null,
|
$roomId: timelineSet.room.roomId,
|
||||||
1,
|
},
|
||||||
Direction.Backward,
|
|
||||||
timelineSet.getFilter(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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) {
|
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);
|
return this.getEventTimeline(timelineSet, event.event_id);
|
||||||
@@ -5376,6 +5525,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
fromToken: string | null,
|
fromToken: string | null,
|
||||||
limit = 30,
|
limit = 30,
|
||||||
dir = Direction.Backward,
|
dir = Direction.Backward,
|
||||||
|
threadListType: ThreadFilterType | null = ThreadFilterType.All,
|
||||||
timelineFilter?: Filter,
|
timelineFilter?: Filter,
|
||||||
): Promise<IMessagesResponse> {
|
): Promise<IMessagesResponse> {
|
||||||
const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId });
|
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> = {
|
const params: Record<string, string> = {
|
||||||
limit: limit.toString(),
|
limit: limit.toString(),
|
||||||
dir: dir,
|
dir: dir,
|
||||||
include: 'all',
|
include: threadFilterTypeToFilter(threadListType),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fromToken) {
|
if (fromToken) {
|
||||||
@@ -5395,7 +5545,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
|
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
|
||||||
// so the timelineFilter doesn't get written into it below
|
// so the timelineFilter doesn't get written into it below
|
||||||
filter = {
|
filter = {
|
||||||
...filter,
|
|
||||||
...Filter.LAZY_LOADING_MESSAGES_FILTER,
|
...Filter.LAZY_LOADING_MESSAGES_FILTER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -5411,14 +5560,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
params.filter = JSON.stringify(filter);
|
params.filter = JSON.stringify(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts: { prefix?: string } = {};
|
const opts = {
|
||||||
if (Thread.hasServerSideListSupport === FeatureSupport.Experimental) {
|
prefix: Thread.hasServerSideListSupport === FeatureSupport.Stable
|
||||||
opts.prefix = "/_matrix/client/unstable/org.matrix.msc3856";
|
? "/_matrix/client/v1"
|
||||||
}
|
: "/_matrix/client/unstable/org.matrix.msc3856",
|
||||||
|
};
|
||||||
|
|
||||||
return this.http.authedRequest<IThreadedMessagesResponse>(Method.Get, path, params, undefined, opts)
|
return this.http.authedRequest<IThreadedMessagesResponse>(Method.Get, path, params, undefined, opts)
|
||||||
.then(res => ({
|
.then(res => ({
|
||||||
...res,
|
...res,
|
||||||
|
chunk: res.chunk?.reverse(),
|
||||||
start: res.prev_batch,
|
start: res.prev_batch,
|
||||||
end: res.next_batch,
|
end: res.next_batch,
|
||||||
}));
|
}));
|
||||||
@@ -5440,7 +5591,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
|
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
|
||||||
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
|
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
|
||||||
const room = this.getRoom(eventTimeline.getRoomId()!);
|
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
|
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||||
// nicely with HTTP errors.
|
// nicely with HTTP errors.
|
||||||
@@ -5511,16 +5663,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
eventTimeline.paginationRequests[dir] = null;
|
eventTimeline.paginationRequests[dir] = null;
|
||||||
});
|
});
|
||||||
eventTimeline.paginationRequests[dir] = promise;
|
eventTimeline.paginationRequests[dir] = promise;
|
||||||
} else if (isThreadTimeline) {
|
} else if (threadListType !== null) {
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
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(
|
promise = this.createThreadListMessagesRequest(
|
||||||
eventTimeline.getRoomId()!,
|
eventTimeline.getRoomId()!,
|
||||||
token,
|
token,
|
||||||
opts.limit,
|
opts.limit,
|
||||||
dir,
|
dir,
|
||||||
|
threadListType,
|
||||||
eventTimeline.getFilter(),
|
eventTimeline.getFilter(),
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
if (res.state) {
|
if (res.state) {
|
||||||
@@ -5547,6 +5704,45 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
eventTimeline.paginationRequests[dir] = null;
|
eventTimeline.paginationRequests[dir] = null;
|
||||||
});
|
});
|
||||||
eventTimeline.paginationRequests[dir] = promise;
|
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 {
|
} else {
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
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 matrixEvents = res.chunk.map(this.getEventMapper());
|
||||||
|
|
||||||
const timelineSet = eventTimeline.getTimelineSet();
|
const timelineSet = eventTimeline.getTimelineSet();
|
||||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||||
this.processBeaconEvents(room, timelineEvents);
|
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;
|
const atEnd = res.end === undefined || res.end === res.start;
|
||||||
|
|
||||||
@@ -6654,25 +6852,40 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
public async doesServerSupportThread(): Promise<{
|
public async doesServerSupportThread(): Promise<{
|
||||||
threads: FeatureSupport;
|
threads: FeatureSupport;
|
||||||
list: FeatureSupport;
|
list: FeatureSupport;
|
||||||
|
fwdPagination: FeatureSupport;
|
||||||
}> {
|
}> {
|
||||||
|
if (await this.isVersionSupported("v1.4")) {
|
||||||
|
return {
|
||||||
|
threads: FeatureSupport.Stable,
|
||||||
|
list: FeatureSupport.Stable,
|
||||||
|
fwdPagination: FeatureSupport.Stable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
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"),
|
||||||
this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
|
this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
|
||||||
this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
|
this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
|
||||||
this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"),
|
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 {
|
return {
|
||||||
threads: determineFeatureSupport(threadStable, threadUnstable),
|
threads: determineFeatureSupport(threadStable, threadUnstable),
|
||||||
list: determineFeatureSupport(listStable, listUnstable),
|
list: determineFeatureSupport(listStable, listUnstable),
|
||||||
|
fwdPagination: determineFeatureSupport(fwdPaginationStable, fwdPaginationUnstable),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
threads: FeatureSupport.None,
|
threads: FeatureSupport.None,
|
||||||
list: FeatureSupport.None,
|
list: FeatureSupport.None,
|
||||||
|
fwdPagination: FeatureSupport.None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6732,12 +6945,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
eventType?: EventType | string | null,
|
eventType?: EventType | string | null,
|
||||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||||
): Promise<{
|
): Promise<{
|
||||||
originalEvent?: MatrixEvent;
|
originalEvent?: MatrixEvent | null;
|
||||||
events: MatrixEvent[];
|
events: MatrixEvent[];
|
||||||
nextBatch?: string;
|
nextBatch?: string | null;
|
||||||
prevBatch?: string;
|
prevBatch?: string | null;
|
||||||
}> {
|
}> {
|
||||||
const fetchedEventType = this.getEncryptedIfNeededEventType(roomId, eventType);
|
const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null;
|
||||||
const result = await this.fetchRelations(
|
const result = await this.fetchRelations(
|
||||||
roomId,
|
roomId,
|
||||||
eventId,
|
eventId,
|
||||||
@@ -6761,10 +6974,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
originalEvent,
|
originalEvent: originalEvent ?? null,
|
||||||
events,
|
events,
|
||||||
nextBatch: result.next_batch,
|
nextBatch: result.next_batch ?? null,
|
||||||
prevBatch: result.prev_batch,
|
prevBatch: result.prev_batch ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7281,7 +7494,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
eventType?: EventType | string | null,
|
eventType?: EventType | string | null,
|
||||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||||
): Promise<IRelationsResponse> {
|
): 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";
|
let templatedUrl = "/rooms/$roomId/relations/$eventId";
|
||||||
if (relationType !== null) {
|
if (relationType !== null) {
|
||||||
@@ -7327,7 +7544,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @return {Promise} Resolves to an object containing the event.
|
* @return {Promise} Resolves to an object containing the event.
|
||||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
* @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(
|
const path = utils.encodeUri(
|
||||||
"/rooms/$roomId/event/$eventId", {
|
"/rooms/$roomId/event/$eventId", {
|
||||||
$roomId: roomId,
|
$roomId: roomId,
|
||||||
|
@@ -27,7 +27,7 @@ import { RoomState } from "./room-state";
|
|||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
import { RelationsContainer } from "./relations-container";
|
import { RelationsContainer } from "./relations-container";
|
||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
import { Thread } from "./thread";
|
import { Thread, ThreadFilterType } from "./thread";
|
||||||
|
|
||||||
const DEBUG = true;
|
const DEBUG = true;
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
opts: IOpts = {},
|
opts: IOpts = {},
|
||||||
client?: MatrixClient,
|
client?: MatrixClient,
|
||||||
public readonly thread?: Thread,
|
public readonly thread?: Thread,
|
||||||
public readonly isThreadTimeline: boolean = false,
|
public readonly threadListType: ThreadFilterType | null = null,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -297,8 +297,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||||
* the given event, or null if unknown
|
* the given event, or null if unknown
|
||||||
*/
|
*/
|
||||||
public getTimelineForEvent(eventId: string | null): EventTimeline | null {
|
public getTimelineForEvent(eventId?: string): EventTimeline | null {
|
||||||
if (eventId === null) { return null; }
|
if (eventId === null || eventId === undefined) { return null; }
|
||||||
const res = this._eventIdToTimeline.get(eventId);
|
const res = this._eventIdToTimeline.get(eventId);
|
||||||
return (res === undefined) ? null : res;
|
return (res === undefined) ? null : res;
|
||||||
}
|
}
|
||||||
@@ -359,7 +359,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
events: MatrixEvent[],
|
events: MatrixEvent[],
|
||||||
toStartOfTimeline: boolean,
|
toStartOfTimeline: boolean,
|
||||||
timeline: EventTimeline,
|
timeline: EventTimeline,
|
||||||
paginationToken?: string,
|
paginationToken?: string | null,
|
||||||
): void {
|
): void {
|
||||||
if (!timeline) {
|
if (!timeline) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@@ -1542,11 +1542,16 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
|||||||
/**
|
/**
|
||||||
* @experimental
|
* @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.thread = thread;
|
||||||
this.setThreadId(thread.id);
|
this.setThreadId(thread?.id);
|
||||||
|
if (thread) {
|
||||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
|
this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
@@ -1555,7 +1560,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
|||||||
return this.thread;
|
return this.thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setThreadId(threadId: string): void {
|
public setThreadId(threadId?: string): void {
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -146,6 +146,7 @@ export type RoomEmittedEvents = RoomEvent
|
|||||||
| ThreadEvent.New
|
| ThreadEvent.New
|
||||||
| ThreadEvent.Update
|
| ThreadEvent.Update
|
||||||
| ThreadEvent.NewReply
|
| ThreadEvent.NewReply
|
||||||
|
| ThreadEvent.Delete
|
||||||
| MatrixEventEvent.BeforeRedaction
|
| MatrixEventEvent.BeforeRedaction
|
||||||
| BeaconEvent.New
|
| BeaconEvent.New
|
||||||
| BeaconEvent.Update
|
| BeaconEvent.Update
|
||||||
@@ -180,7 +181,7 @@ export type RoomEventHandlerMap = {
|
|||||||
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
ThreadHandlerMap,
|
ThreadHandlerMap,
|
||||||
ThreadEvent.Update | ThreadEvent.NewReply
|
ThreadEvent.Update | ThreadEvent.NewReply | ThreadEvent.Delete
|
||||||
>
|
>
|
||||||
& EventTimelineSetHandlerMap
|
& EventTimelineSetHandlerMap
|
||||||
& Pick<MatrixEventHandlerMap, MatrixEventEvent.BeforeRedaction>
|
& 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).
|
* 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.
|
* 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++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
this.timelineSets[i].resetLiveTimeline(
|
this.timelineSets[i].resetLiveTimeline(
|
||||||
backPaginationToken ?? undefined,
|
backPaginationToken ?? undefined,
|
||||||
@@ -1651,7 +1652,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
let timelineSet: EventTimelineSet;
|
let timelineSet: EventTimelineSet;
|
||||||
if (Thread.hasServerSideListSupport) {
|
if (Thread.hasServerSideListSupport) {
|
||||||
timelineSet =
|
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, [
|
this.reEmitter.reEmit(timelineSet, [
|
||||||
RoomEvent.Timeline,
|
RoomEvent.Timeline,
|
||||||
RoomEvent.TimelineReset,
|
RoomEvent.TimelineReset,
|
||||||
@@ -1758,7 +1759,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
let latestMyThreadsRootEvent: MatrixEvent | undefined;
|
let latestMyThreadsRootEvent: MatrixEvent | undefined;
|
||||||
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||||
for (const rootEvent of threadRoots) {
|
for (const rootEvent of threadRoots) {
|
||||||
this.threadsTimelineSets[0].addLiveEvent(rootEvent, {
|
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, {
|
||||||
duplicateStrategy: DuplicateStrategy.Ignore,
|
duplicateStrategy: DuplicateStrategy.Ignore,
|
||||||
fromCache: false,
|
fromCache: false,
|
||||||
roomState,
|
roomState,
|
||||||
@@ -1767,7 +1768,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
const threadRelationship = rootEvent
|
const threadRelationship = rootEvent
|
||||||
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||||
if (threadRelationship?.current_user_participated) {
|
if (threadRelationship?.current_user_participated) {
|
||||||
this.threadsTimelineSets[1].addLiveEvent(rootEvent, {
|
this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, {
|
||||||
duplicateStrategy: DuplicateStrategy.Ignore,
|
duplicateStrategy: DuplicateStrategy.Ignore,
|
||||||
fromCache: false,
|
fromCache: false,
|
||||||
roomState,
|
roomState,
|
||||||
@@ -1785,6 +1786,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
|
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
|
||||||
|
this.on(ThreadEvent.Delete, this.onThreadDelete);
|
||||||
this.threadsReady = true;
|
this.threadsReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1803,6 +1805,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
null,
|
null,
|
||||||
undefined,
|
undefined,
|
||||||
Direction.Backward,
|
Direction.Backward,
|
||||||
|
timelineSet.threadListType,
|
||||||
timelineSet.getFilter(),
|
timelineSet.getFilter(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1823,14 +1826,21 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onThreadNewReply(thread: Thread): void {
|
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) {
|
for (const timelineSet of this.threadsTimelineSets) {
|
||||||
timelineSet.removeEvent(thread.id);
|
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 {
|
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
|
||||||
let thread = this.getThread(threadId);
|
let thread = this.getThread(threadId);
|
||||||
|
|
||||||
if (thread) {
|
if (!thread) {
|
||||||
thread.addEvents(events, toStartOfTimeline);
|
|
||||||
} else {
|
|
||||||
const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
|
const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
|
||||||
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
|
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(
|
public createThread(
|
||||||
threadId: string,
|
threadId: string,
|
||||||
rootEvent: MatrixEvent | undefined,
|
rootEvent: MatrixEvent | undefined,
|
||||||
@@ -1958,38 +1998,37 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const thread = new Thread(threadId, rootEvent, {
|
const thread = new Thread(threadId, rootEvent, {
|
||||||
initialEvents: events,
|
|
||||||
room: this,
|
room: this,
|
||||||
client: this.client,
|
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
|
// If we managed to create a thread and figure out its `id` then we can use it
|
||||||
this.threads.set(thread.id, thread);
|
this.threads.set(thread.id, thread);
|
||||||
this.reEmitter.reEmit(thread, [
|
this.reEmitter.reEmit(thread, [
|
||||||
|
ThreadEvent.Delete,
|
||||||
ThreadEvent.Update,
|
ThreadEvent.Update,
|
||||||
ThreadEvent.NewReply,
|
ThreadEvent.NewReply,
|
||||||
RoomEvent.Timeline,
|
RoomEvent.Timeline,
|
||||||
RoomEvent.TimelineReset,
|
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;
|
this.lastThread = thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.threadsReady) {
|
if (this.threadsReady) {
|
||||||
this.threadsTimelineSets.forEach(timelineSet => {
|
this.updateThreadRootEvents(thread, toStartOfTimeline);
|
||||||
if (thread.rootEvent) {
|
|
||||||
if (Thread.hasServerSideSupport) {
|
|
||||||
timelineSet.addLiveEvent(thread.rootEvent);
|
|
||||||
} else {
|
|
||||||
timelineSet.addEventToTimeline(
|
|
||||||
thread.rootEvent,
|
|
||||||
timelineSet.getLiveTimeline(),
|
|
||||||
toStartOfTimeline,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit(ThreadEvent.New, 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 { MatrixClient, MatrixEventEvent, RelationType, RoomEvent } from "../matrix";
|
||||||
import { TypedReEmitter } from "../ReEmitter";
|
import { TypedReEmitter } from "../ReEmitter";
|
||||||
import { IRelationsRequestOpts } from "../@types/requests";
|
|
||||||
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||||
import { Direction, EventTimeline } from "./event-timeline";
|
import { EventTimeline } from "./event-timeline";
|
||||||
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
|
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
|
||||||
import { Room } from './room';
|
import { Room } from './room';
|
||||||
import { RoomState } from "./room-state";
|
import { RoomState } from "./room-state";
|
||||||
@@ -33,6 +32,7 @@ export enum ThreadEvent {
|
|||||||
Update = "Thread.update",
|
Update = "Thread.update",
|
||||||
NewReply = "Thread.newReply",
|
NewReply = "Thread.newReply",
|
||||||
ViewThread = "Thread.viewThread",
|
ViewThread = "Thread.viewThread",
|
||||||
|
Delete = "Thread.delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New>
|
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New>
|
||||||
@@ -43,10 +43,10 @@ export type EventHandlerMap = {
|
|||||||
[ThreadEvent.Update]: (thread: Thread) => void;
|
[ThreadEvent.Update]: (thread: Thread) => void;
|
||||||
[ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
|
[ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
|
||||||
[ThreadEvent.ViewThread]: () => void;
|
[ThreadEvent.ViewThread]: () => void;
|
||||||
|
[ThreadEvent.Delete]: (thread: Thread) => void;
|
||||||
} & EventTimelineSetHandlerMap;
|
} & EventTimelineSetHandlerMap;
|
||||||
|
|
||||||
interface IThreadOpts {
|
interface IThreadOpts {
|
||||||
initialEvents?: MatrixEvent[];
|
|
||||||
room: Room;
|
room: Room;
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,7 @@ export function determineFeatureSupport(stable: boolean, unstable: boolean): Fea
|
|||||||
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||||
public static hasServerSideSupport = FeatureSupport.None;
|
public static hasServerSideSupport = FeatureSupport.None;
|
||||||
public static hasServerSideListSupport = 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
|
* 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 reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
|
||||||
|
|
||||||
private lastEvent!: MatrixEvent;
|
private lastEvent: MatrixEvent | undefined;
|
||||||
private replyCount = 0;
|
private replyCount = 0;
|
||||||
|
|
||||||
public readonly room: Room;
|
public readonly room: Room;
|
||||||
@@ -122,14 +123,10 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
||||||
this.timelineSet.on(RoomEvent.Timeline, 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
|
// 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.
|
// gappy sync and a thread around this event may already exist.
|
||||||
this.initialiseThread();
|
this.initialiseThread();
|
||||||
|
this.setEventMetadata(this.rootEvent);
|
||||||
this.rootEvent?.setThread(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchRootEvent(): Promise<void> {
|
private async fetchRootEvent(): Promise<void> {
|
||||||
@@ -142,13 +139,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to fetch thread root to construct thread with", e);
|
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||||
}
|
}
|
||||||
|
await this.processEvent(this.rootEvent);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static setServerSideSupport(
|
public static setServerSideSupport(
|
||||||
@@ -168,6 +159,12 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
Thread.hasServerSideListSupport = status;
|
Thread.hasServerSideListSupport = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static setServerSideFwdPaginationSupport(
|
||||||
|
status: FeatureSupport,
|
||||||
|
): void {
|
||||||
|
Thread.hasServerSideFwdPaginationSupport = status;
|
||||||
|
}
|
||||||
|
|
||||||
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
|
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
|
||||||
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
|
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||||
this.room.eventShouldLiveIn(event).threadId === this.id &&
|
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
|
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
|
||||||
const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse();
|
if (this.replyCount <= 0) {
|
||||||
this.lastEvent = events.find(e => (
|
for (const threadEvent of this.events) {
|
||||||
!e.isRedacted() &&
|
this.clearEventMetadata(threadEvent);
|
||||||
e.isRelation(THREAD_RELATION_TYPE.name)
|
}
|
||||||
)) ?? this.rootEvent!;
|
this.lastEvent = this.rootEvent;
|
||||||
this.emit(ThreadEvent.Update, this);
|
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 (event.threadRootId !== this.id) return; // ignore echoes for other timelines
|
||||||
if (this.lastEvent === event) return;
|
if (this.lastEvent === event) return;
|
||||||
if (!event.isRelation(THREAD_RELATION_TYPE.name)) return;
|
if (!event.isRelation(THREAD_RELATION_TYPE.name)) return;
|
||||||
|
|
||||||
// There is a risk that the `localTimestamp` approximation will not be accurate
|
await this.initialiseThread();
|
||||||
// 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.NewReply, this, event);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ThreadEvent.Update, this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public get roomState(): RoomState {
|
public get roomState(): RoomState {
|
||||||
@@ -237,7 +219,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
|
|
||||||
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||||
events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
|
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.
|
* 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.
|
* @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 {
|
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> {
|
||||||
event.setThread(this);
|
this.setEventMetadata(event);
|
||||||
|
|
||||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
const lastReply = this.lastReply();
|
||||||
this._currentUserParticipated = true;
|
const isNewestReply = !lastReply || event.localTimestamp > lastReply!.localTimestamp;
|
||||||
}
|
|
||||||
|
|
||||||
// Add all incoming events to the thread's timeline set when there's no server support
|
// Add all incoming events to the thread's timeline set when there's no server support
|
||||||
if (!Thread.hasServerSideSupport) {
|
if (!Thread.hasServerSideSupport) {
|
||||||
@@ -265,16 +246,13 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
this.addEventToTimeline(event, toStartOfTimeline);
|
this.addEventToTimeline(event, toStartOfTimeline);
|
||||||
|
|
||||||
this.client.decryptEventIfNeeded(event, {});
|
this.client.decryptEventIfNeeded(event, {});
|
||||||
} else if (!toStartOfTimeline &&
|
} else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) {
|
||||||
this.initialEventsFetched &&
|
await this.fetchEditsWhereNeeded(event);
|
||||||
event.localTimestamp > this.lastReply()!.localTimestamp
|
|
||||||
) {
|
|
||||||
this.fetchEditsWhereNeeded(event);
|
|
||||||
this.addEventToTimeline(event, false);
|
this.addEventToTimeline(event, false);
|
||||||
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
|
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
|
||||||
// Apply annotations and replace relations to the relations of the timeline only
|
// Apply annotations and replace relations to the relations of the timeline only
|
||||||
this.timelineSet.relations.aggregateParentEvent(event);
|
this.timelineSet.relations?.aggregateParentEvent(event);
|
||||||
this.timelineSet.relations.aggregateChildEvent(event, this.timelineSet);
|
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +263,15 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (emit) {
|
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);
|
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initialiseThread(): Promise<void> {
|
public async initialiseThread(): Promise<void> {
|
||||||
let bundledRelationship = this.getRootEventBundledRelationship();
|
let bundledRelationship = this.getRootEventBundledRelationship();
|
||||||
if (Thread.hasServerSideSupport && !bundledRelationship) {
|
if (Thread.hasServerSideSupport) {
|
||||||
await this.fetchRootEvent();
|
await this.fetchRootEvent();
|
||||||
bundledRelationship = this.getRootEventBundledRelationship();
|
bundledRelationship = this.getRootEventBundledRelationship();
|
||||||
}
|
}
|
||||||
@@ -304,15 +290,25 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
this.replyCount = bundledRelationship.count;
|
this.replyCount = bundledRelationship.count;
|
||||||
this._currentUserParticipated = !!bundledRelationship.current_user_participated;
|
this._currentUserParticipated = !!bundledRelationship.current_user_participated;
|
||||||
|
|
||||||
const event = new MatrixEvent({
|
const mapper = this.client.getEventMapper();
|
||||||
room_id: this.room.roomId,
|
this.lastEvent = mapper(bundledRelationship.latest_event);
|
||||||
...bundledRelationship.latest_event,
|
await this.processEvent(this.lastEvent);
|
||||||
});
|
}
|
||||||
this.setEventMetadata(event);
|
|
||||||
event.setThread(this);
|
|
||||||
this.lastEvent = event;
|
|
||||||
|
|
||||||
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);
|
this.emit(ThreadEvent.Update, this);
|
||||||
@@ -334,16 +330,19 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchInitialEvents(): Promise<void> {
|
public setEventMetadata(event: Optional<MatrixEvent>): void {
|
||||||
if (this.initialEventsFetched) return;
|
if (event) {
|
||||||
await this.fetchEvents();
|
|
||||||
this.initialEventsFetched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setEventMetadata(event: MatrixEvent): void {
|
|
||||||
EventTimeline.setEventMetadata(event, this.roomState, false);
|
EventTimeline.setEventMetadata(event, this.roomState, false);
|
||||||
event.setThread(this);
|
event.setThread(this);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearEventMetadata(event: Optional<MatrixEvent>): void {
|
||||||
|
if (event) {
|
||||||
|
event.setThread(undefined);
|
||||||
|
delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an event by ID in the current thread
|
* Finds an event by ID in the current thread
|
||||||
@@ -406,55 +405,6 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
return this.timelineSet.getLiveTimeline();
|
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 {
|
public getUnfilteredTimelineSet(): EventTimelineSet {
|
||||||
return this.timelineSet;
|
return this.timelineSet;
|
||||||
}
|
}
|
||||||
@@ -485,3 +435,12 @@ export enum ThreadFilterType {
|
|||||||
"My",
|
"My",
|
||||||
"All"
|
"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,
|
// 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.
|
// which is important to keep room-switching feeling snappy.
|
||||||
if (initialEventId) {
|
if (this.timelineSet.getTimelineForEvent(initialEventId)) {
|
||||||
const timeline = this.timelineSet.getTimelineForEvent(initialEventId);
|
initFields(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 Promise.resolve();
|
||||||
}
|
} else if (initialEventId) {
|
||||||
|
return this.client.getEventTimeline(this.timelineSet, initialEventId)
|
||||||
return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields);
|
.then(initFields);
|
||||||
} else {
|
} else {
|
||||||
const tl = this.timelineSet.getLiveTimeline();
|
initFields(this.timelineSet.getLiveTimeline());
|
||||||
initFields(tl);
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,8 +232,9 @@ export class TimelineWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
|
const hasNeighbouringTimeline = tl.timeline.getNeighbouringTimeline(direction);
|
||||||
tl.timeline.getPaginationToken(direction) !== null);
|
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
|
* @return {Promise} Resolves to a boolean which is true if more events
|
||||||
* were successfully retrieved.
|
* were successfully retrieved.
|
||||||
*/
|
*/
|
||||||
public paginate(
|
public async paginate(
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
size: number,
|
size: number,
|
||||||
makeRequest = true,
|
makeRequest = true,
|
||||||
@@ -274,7 +271,7 @@ export class TimelineWindow {
|
|||||||
|
|
||||||
if (!tl) {
|
if (!tl) {
|
||||||
debuglog("TimelineWindow: no timeline yet");
|
debuglog("TimelineWindow: no timeline yet");
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tl.pendingPaginate) {
|
if (tl.pendingPaginate) {
|
||||||
@@ -283,20 +280,20 @@ export class TimelineWindow {
|
|||||||
|
|
||||||
// try moving the cap
|
// try moving the cap
|
||||||
if (this.extend(direction, size)) {
|
if (this.extend(direction, size)) {
|
||||||
return Promise.resolve(true);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!makeRequest || requestLimit === 0) {
|
if (!makeRequest || requestLimit === 0) {
|
||||||
// todo: should we return something different to indicate that there
|
// todo: should we return something different to indicate that there
|
||||||
// might be more events out there, but we haven't found them yet?
|
// might be more events out there, but we haven't found them yet?
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// try making a pagination request
|
// try making a pagination request
|
||||||
const token = tl.timeline.getPaginationToken(direction);
|
const token = tl.timeline.getPaginationToken(direction);
|
||||||
if (token === null) {
|
if (!token) {
|
||||||
debuglog("TimelineWindow: no token");
|
debuglog("TimelineWindow: no token");
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("TimelineWindow: starting request");
|
debuglog("TimelineWindow: starting request");
|
||||||
@@ -309,8 +306,7 @@ export class TimelineWindow {
|
|||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
debuglog("TimelineWindow: request completed with result " + r);
|
debuglog("TimelineWindow: request completed with result " + r);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
// end of timeline
|
return this.paginate(direction, size, false, 0);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// recurse to advance the index into the results.
|
// 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 unhomoglyph from "unhomoglyph";
|
||||||
import promiseRetry from "p-retry";
|
import promiseRetry from "p-retry";
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { MatrixEvent } from "./models/event";
|
import { MatrixEvent } from "./models/event";
|
||||||
import { M_TIMESTAMP } from "./@types/location";
|
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>;
|
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.
|
* Decode a query string in `application/x-www-form-urlencoded` format.
|
||||||
* @param {string} query A query string to decode e.g.
|
* @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" }.
|
* variables with. E.g. { "$bar": "baz" }.
|
||||||
* @return {string} The result of replacing all template variables e.g. '/foo/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) {
|
for (const key in variables) {
|
||||||
if (!variables.hasOwnProperty(key)) {
|
if (!variables.hasOwnProperty(key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const value = variables[key];
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
pathTemplate = pathTemplate.replace(
|
pathTemplate = pathTemplate.replace(
|
||||||
key, encodeURIComponent(variables[key]),
|
key, encodeURIComponent(value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pathTemplate;
|
return pathTemplate;
|
||||||
|
Reference in New Issue
Block a user