You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Fix race conditions around threads (#2331)
This commit is contained in:
committed by
GitHub
parent
274d6a9597
commit
ac5fee0a69
@ -811,9 +811,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
},
|
||||
},
|
||||
threads: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
getThread: jest.fn(),
|
||||
addPendingEvent: jest.fn(),
|
||||
updatePendingEvent: jest.fn(),
|
||||
reEmitter: {
|
||||
|
@ -36,7 +36,7 @@ import { RoomState } from "../../src/models/room-state";
|
||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { ThreadEvent } from "../../src/models/thread";
|
||||
import { Thread, ThreadEvent } from "../../src/models/thread";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@ -1914,7 +1914,7 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
room.createThread(undefined, [eventWithoutARootEvent]);
|
||||
room.createThread("$000", undefined, [eventWithoutARootEvent]);
|
||||
|
||||
const rootEvent = new MatrixEvent({
|
||||
event_id: "$666",
|
||||
@ -1932,7 +1932,7 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => room.createThread(rootEvent, [])).not.toThrow();
|
||||
expect(() => room.createThread(rootEvent.getId(), rootEvent, [])).not.toThrow();
|
||||
});
|
||||
|
||||
it("Edits update the lastReply event", async () => {
|
||||
@ -1959,14 +1959,16 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread.replyToEvent).toBe(threadResponse);
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
room.addLiveEvents([threadResponseEdit]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
|
||||
});
|
||||
|
||||
@ -1993,15 +1995,17 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(1);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
});
|
||||
@ -2030,15 +2034,17 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
|
||||
room.addLiveEvents([threadResponse2ReactionRedaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
});
|
||||
@ -2067,15 +2073,17 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadRootRedaction = mkRedaction(threadRoot);
|
||||
room.addLiveEvents([threadRootRedaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(2);
|
||||
});
|
||||
|
||||
@ -2102,21 +2110,24 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
||||
room.addLiveEvents([threadResponse2Redaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(1);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(0);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
|
||||
});
|
||||
@ -2234,5 +2245,45 @@ describe("Room", function() {
|
||||
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should aggregate relations in thread event timeline set", () => {
|
||||
Thread.setServerSideSupport(true, true);
|
||||
const threadRoot = mkMessage();
|
||||
const rootReaction = mkReaction(threadRoot);
|
||||
const threadResponse = mkThreadResponse(threadRoot);
|
||||
const threadReaction = mkReaction(threadResponse);
|
||||
|
||||
const events = [
|
||||
threadRoot,
|
||||
rootReaction,
|
||||
threadResponse,
|
||||
threadReaction,
|
||||
];
|
||||
|
||||
room.addLiveEvents(events);
|
||||
|
||||
const thread = threadRoot.getThread();
|
||||
expect(thread.rootEvent).toBe(threadRoot);
|
||||
|
||||
const rootRelations = thread.timelineSet.getRelationsForEvent(
|
||||
threadRoot.getId(),
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
).getSortedAnnotationsByKey();
|
||||
expect(rootRelations).toHaveLength(1);
|
||||
expect(rootRelations[0][0]).toEqual(rootReaction.getRelation().key);
|
||||
expect(rootRelations[0][1].size).toEqual(1);
|
||||
expect(rootRelations[0][1].has(rootReaction)).toBeTruthy();
|
||||
|
||||
const responseRelations = thread.timelineSet.getRelationsForEvent(
|
||||
threadResponse.getId(),
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
).getSortedAnnotationsByKey();
|
||||
expect(responseRelations).toHaveLength(1);
|
||||
expect(responseRelations[0][0]).toEqual(threadReaction.getRelation().key);
|
||||
expect(responseRelations[0][1].size).toEqual(1);
|
||||
expect(responseRelations[0][1].has(threadReaction)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -48,7 +48,9 @@ import { IRoomEncryption, RoomList } from './crypto/RoomList';
|
||||
import { logger } from './logger';
|
||||
import { SERVICE_TYPES } from './service-types';
|
||||
import {
|
||||
FileType, HttpApiEvent, HttpApiEventHandlerMap,
|
||||
FileType,
|
||||
HttpApiEvent,
|
||||
HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
IUpload,
|
||||
MatrixError,
|
||||
@ -3741,7 +3743,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
"rel_type": THREAD_RELATION_TYPE.name,
|
||||
"event_id": threadId,
|
||||
};
|
||||
const thread = this.getRoom(roomId)?.threads.get(threadId);
|
||||
const thread = this.getRoom(roomId)?.getThread(threadId);
|
||||
if (thread) {
|
||||
content["m.relates_to"]["m.in_reply_to"] = {
|
||||
"event_id": thread.lastReply((ev: MatrixEvent) => {
|
||||
@ -3790,7 +3792,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}));
|
||||
|
||||
const room = this.getRoom(roomId);
|
||||
const thread = room?.threads.get(threadId);
|
||||
const thread = room?.getThread(threadId);
|
||||
if (thread) {
|
||||
localEvent.setThread(thread);
|
||||
}
|
||||
@ -5185,7 +5187,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
limit,
|
||||
Direction.Backward,
|
||||
);
|
||||
}).then(async (res: IMessagesResponse) => {
|
||||
}).then((res: IMessagesResponse) => {
|
||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||
if (res.state) {
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
@ -5196,7 +5198,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
await this.processThreadEvents(room, threadedEvents, true);
|
||||
this.processThreadEvents(room, threadedEvents, true);
|
||||
|
||||
room.oldState.paginationToken = res.end;
|
||||
if (res.chunk.length === 0) {
|
||||
@ -5299,25 +5301,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
event.isRelation(THREAD_RELATION_TYPE.name)
|
||||
) {
|
||||
const [, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
|
||||
const thread = await timelineSet.room.createThreadFetchRoot(event.threadRootId, threadedEvents, true);
|
||||
|
||||
let nextBatch: string;
|
||||
const response = await thread.fetchInitialEvents();
|
||||
if (response?.nextBatch) {
|
||||
nextBatch = response.nextBatch;
|
||||
let thread = timelineSet.room.getThread(event.threadRootId);
|
||||
if (!thread) {
|
||||
thread = timelineSet.room.createThread(event.threadRootId, undefined, threadedEvents, true);
|
||||
}
|
||||
|
||||
const opts: IRelationsRequestOpts = {
|
||||
direction: Direction.Backward,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
// Fetch events until we find the one we were asked for
|
||||
await thread.fetchInitialEvents();
|
||||
let nextBatch = 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;
|
||||
@ -5336,7 +5340,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
|
||||
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
|
||||
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
@ -5493,7 +5497,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
opts.limit,
|
||||
dir,
|
||||
eventTimeline.getFilter(),
|
||||
).then(async (res) => {
|
||||
).then((res) => {
|
||||
if (res.state) {
|
||||
const roomState = eventTimeline.getState(dir);
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
@ -5506,7 +5510,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, backwards);
|
||||
this.processThreadEvents(room, threadedEvents, backwards);
|
||||
|
||||
// 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
|
||||
@ -6663,7 +6667,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventId: string,
|
||||
relationType?: RelationType | string | null,
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = {},
|
||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
||||
): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
@ -7204,7 +7208,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventId: string,
|
||||
relationType?: RelationType | string | null,
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = {},
|
||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
||||
): Promise<IRelationsResponse> {
|
||||
const queryString = utils.encodeParams(opts as Record<string, string | number>);
|
||||
|
||||
@ -8916,12 +8920,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public async processThreadEvents(
|
||||
room: Room,
|
||||
threadedEvents: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
): Promise<void> {
|
||||
await room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
public processThreadEvents(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
public processBeaconEvents(
|
||||
|
@ -852,14 +852,13 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
}
|
||||
let relationsWithEventType = relationsWithRelType[eventType];
|
||||
|
||||
let relatesToEvent: MatrixEvent;
|
||||
if (!relationsWithEventType) {
|
||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||
relationType,
|
||||
eventType,
|
||||
this.room,
|
||||
);
|
||||
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||
const relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||
if (relatesToEvent) {
|
||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||
}
|
||||
|
@ -1303,8 +1303,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
public isRelation(relType: string = undefined): boolean {
|
||||
// Relation info is lifted out of the encrypted content when sent to
|
||||
// encrypted rooms, so we have to check `getWireContent` for this.
|
||||
const content = this.getWireContent();
|
||||
const relation = content && content["m.relates_to"];
|
||||
const relation = this.getWireContent()?.["m.relates_to"];
|
||||
return relation && relation.rel_type && relation.event_id &&
|
||||
((relType && relation.rel_type === relType) || !relType);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { defer, normalize } from "../utils";
|
||||
import { normalize } from "../utils";
|
||||
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { RoomMember } from "./room-member";
|
||||
@ -214,8 +214,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
private getTypeWarning = false;
|
||||
private getVersionWarning = false;
|
||||
private membersPromise?: Promise<boolean>;
|
||||
// Map from threadId to pending Thread instance created by createThreadFetchRoot
|
||||
private threadPromises = new Map<string, Promise<Thread>>();
|
||||
|
||||
// XXX: These should be read-only
|
||||
/**
|
||||
@ -266,7 +264,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public threads = new Map<string, Thread>();
|
||||
private threads = new Map<string, Thread>();
|
||||
public lastThread: Thread;
|
||||
|
||||
/**
|
||||
@ -1208,9 +1206,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
* @experimental
|
||||
*/
|
||||
public getThread(eventId: string): Thread {
|
||||
return this.getThreads().find(thread => {
|
||||
return thread.id === eventId;
|
||||
});
|
||||
return this.threads.get(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1524,7 +1520,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
|
||||
if (!this.getThread(rootEvent.getId())) {
|
||||
this.createThread(rootEvent, [], true);
|
||||
this.createThread(rootEvent.getId(), rootEvent, [], true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1620,58 +1616,14 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
return threadId ? this.getThread(threadId) : null;
|
||||
}
|
||||
|
||||
public async createThreadFetchRoot(
|
||||
threadId: string,
|
||||
events?: MatrixEvent[],
|
||||
toStartOfTimeline?: boolean,
|
||||
): Promise<Thread | null> {
|
||||
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
|
||||
let thread = this.getThread(threadId);
|
||||
|
||||
if (!thread) {
|
||||
const deferred = defer<Thread | null>();
|
||||
this.threadPromises.set(threadId, deferred.promise);
|
||||
|
||||
let rootEvent = this.findEventById(threadId);
|
||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||
try {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
|
||||
const mapper = this.client.getEventMapper();
|
||||
rootEvent = mapper(eventData); // will merge with existing event object if such is known
|
||||
} catch (e) {
|
||||
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||
} finally {
|
||||
this.threadPromises.delete(threadId);
|
||||
// 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
|
||||
thread = this.createThread(rootEvent, events, toStartOfTimeline);
|
||||
if (thread) {
|
||||
rootEvent?.setThread(thread);
|
||||
}
|
||||
deferred.resolve(thread);
|
||||
}
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
private async addThreadedEvents(events: MatrixEvent[], threadId: string, toStartOfTimeline = false): Promise<void> {
|
||||
let thread = this.getThread(threadId);
|
||||
if (this.threadPromises.has(threadId)) {
|
||||
thread = await this.threadPromises.get(threadId);
|
||||
}
|
||||
|
||||
events = events.filter(e => e.getId() !== threadId); // filter out any root events
|
||||
|
||||
if (thread) {
|
||||
for (const event of events) {
|
||||
await thread.addEvent(event, toStartOfTimeline);
|
||||
}
|
||||
thread.addEvents(events, toStartOfTimeline);
|
||||
} else {
|
||||
thread = await this.createThreadFetchRoot(threadId, events, toStartOfTimeline);
|
||||
}
|
||||
|
||||
if (thread) {
|
||||
const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
|
||||
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
}
|
||||
}
|
||||
@ -1680,30 +1632,29 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
* Adds events to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public async processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): Promise<unknown> {
|
||||
public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
events.forEach(this.applyRedaction);
|
||||
|
||||
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||
for (const event of events) {
|
||||
const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event);
|
||||
if (shouldLiveInThread) {
|
||||
if (!eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId].push(event);
|
||||
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId]?.push(event);
|
||||
}
|
||||
|
||||
return Promise.all(Object.entries(eventsByThread).map(([threadId, events]) => (
|
||||
this.addThreadedEvents(events, threadId, toStartOfTimeline)
|
||||
)));
|
||||
Object.entries(eventsByThread).map(([threadId, events]) => (
|
||||
this.addThreadedEvents(threadId, events, toStartOfTimeline)
|
||||
));
|
||||
}
|
||||
|
||||
public createThread(
|
||||
threadId: string,
|
||||
rootEvent: MatrixEvent | undefined,
|
||||
events: MatrixEvent[] = [],
|
||||
toStartOfTimeline: boolean,
|
||||
): Thread | undefined {
|
||||
): Thread {
|
||||
if (rootEvent) {
|
||||
const tl = this.getTimelineForEvent(rootEvent.getId());
|
||||
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
|
||||
@ -1712,45 +1663,44 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
}
|
||||
|
||||
const thread = new Thread(rootEvent, {
|
||||
const thread = new Thread(threadId, rootEvent, {
|
||||
initialEvents: events,
|
||||
room: this,
|
||||
client: this.client,
|
||||
});
|
||||
|
||||
// If we managed to create a thread and figure out its `id` then we can use it
|
||||
if (thread.id) {
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.NewReply,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.NewReply,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
|
||||
|
||||
if (this.threadsReady) {
|
||||
this.threadsTimelineSets.forEach(timelineSet => {
|
||||
if (thread.rootEvent) {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
timelineSet.addLiveEvent(thread.rootEvent);
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(
|
||||
thread.rootEvent,
|
||||
timelineSet.getLiveTimeline(),
|
||||
toStartOfTimeline,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return thread;
|
||||
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
|
||||
|
||||
if (this.threadsReady) {
|
||||
this.threadsTimelineSets.forEach(timelineSet => {
|
||||
if (thread.rootEvent) {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
timelineSet.addLiveEvent(thread.rootEvent);
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(
|
||||
thread.rootEvent,
|
||||
timelineSet.getLiveTimeline(),
|
||||
toStartOfTimeline,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
private applyRedaction = (event: MatrixEvent): void => {
|
||||
@ -2191,7 +2141,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
const threadInfos = events.map(e => this.eventShouldLiveIn(e, events, threadRoots));
|
||||
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
@ -2202,14 +2151,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
shouldLiveInRoom,
|
||||
shouldLiveInThread,
|
||||
threadId,
|
||||
} = threadInfos[i];
|
||||
} = this.eventShouldLiveIn(events[i], events, threadRoots);
|
||||
|
||||
if (shouldLiveInThread) {
|
||||
if (!eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId].push(events[i]);
|
||||
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId]?.push(events[i]);
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
|
||||
@ -2217,7 +2164,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
|
||||
Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
|
||||
this.addThreadedEvents(threadEvents, threadId, false);
|
||||
this.addThreadedEvents(threadId, threadEvents, false);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -70,12 +70,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
public readonly room: Room;
|
||||
public readonly client: MatrixClient;
|
||||
|
||||
public initialEventsFetched = false;
|
||||
|
||||
public readonly id: string;
|
||||
public initialEventsFetched = !Thread.hasServerSideSupport;
|
||||
|
||||
constructor(
|
||||
public readonly rootEvent: MatrixEvent | undefined,
|
||||
public readonly id: string,
|
||||
public rootEvent: MatrixEvent | undefined,
|
||||
opts: IThreadOpts,
|
||||
) {
|
||||
super();
|
||||
@ -99,12 +98,33 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
||||
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
|
||||
|
||||
// If we weren't able to find the root event, it's probably missing,
|
||||
// and we define the thread ID from one of the thread relation
|
||||
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
|
||||
this.initialiseThread(this.rootEvent);
|
||||
if (opts.initialEvents) {
|
||||
this.addEvents(opts.initialEvents, false);
|
||||
}
|
||||
// even if this thread is thought to be originating from this client, we initialise it as we may be in a
|
||||
// gappy sync and a thread around this event may already exist.
|
||||
this.initialiseThread();
|
||||
|
||||
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
||||
this.rootEvent?.setThread(this);
|
||||
}
|
||||
|
||||
private async fetchRootEvent(): Promise<void> {
|
||||
this.rootEvent = this.room.findEventById(this.id);
|
||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||
try {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
|
||||
const mapper = this.client.getEventMapper();
|
||||
this.rootEvent = mapper(eventData); // will merge with existing event object if such is known
|
||||
} catch (e) {
|
||||
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||
}
|
||||
|
||||
// The root event might be not be visible to the person requesting it.
|
||||
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
|
||||
// benefit from all the APIs a homeserver can provide to enhance the thread experience
|
||||
this.rootEvent?.setThread(this);
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
|
||||
@ -180,6 +200,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
@ -187,43 +212,59 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
* @param event The event to add
|
||||
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||
* to the start (and not the end) of the timeline.
|
||||
* @param {boolean} emit whether to emit the Update event if the thread was updated or not.
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
||||
public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): void {
|
||||
event.setThread(this);
|
||||
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
// Add all annotations and replace relations to the timeline so that the relations are processed accordingly
|
||||
if ([RelationType.Annotation, RelationType.Replace].includes(event.getRelation()?.rel_type as RelationType)) {
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add all incoming events to the thread's timeline set when there's no server support
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
|
||||
event.setThread(this);
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
this.client.decryptEventIfNeeded(event, {});
|
||||
} else if (!toStartOfTimeline &&
|
||||
this.initialEventsFetched &&
|
||||
event.localTimestamp > this.lastReply().localTimestamp
|
||||
event.localTimestamp > this.lastReply()?.localTimestamp
|
||||
) {
|
||||
await this.fetchEditsWhereNeeded(event);
|
||||
this.fetchEditsWhereNeeded(event);
|
||||
this.addEventToTimeline(event, false);
|
||||
}
|
||||
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
// If no thread support exists we want to count all thread relation
|
||||
// added as a reply. We can't rely on the bundled relationships count
|
||||
if (!Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
if (emit) {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
}
|
||||
|
||||
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
|
||||
const bundledRelationship = rootEvent
|
||||
?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship {
|
||||
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
}
|
||||
|
||||
private async initialiseThread(): Promise<void> {
|
||||
let bundledRelationship = this.getRootEventBundledRelationship();
|
||||
if (Thread.hasServerSideSupport && !bundledRelationship) {
|
||||
await this.fetchRootEvent();
|
||||
bundledRelationship = this.getRootEventBundledRelationship();
|
||||
}
|
||||
|
||||
if (Thread.hasServerSideSupport && bundledRelationship) {
|
||||
this.replyCount = bundledRelationship.count;
|
||||
@ -236,6 +277,8 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
|
||||
this.fetchEditsWhereNeeded(event);
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
|
||||
@ -253,24 +296,10 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
}));
|
||||
}
|
||||
|
||||
public async fetchInitialEvents(): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
} | null> {
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
this.initialEventsFetched = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
return response;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
public async fetchInitialEvents(): Promise<void> {
|
||||
if (this.initialEventsFetched) return;
|
||||
await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
}
|
||||
|
||||
private setEventMetadata(event: MatrixEvent): void {
|
||||
@ -319,7 +348,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
* A getter for the last event added to the thread
|
||||
*/
|
||||
public get replyToEvent(): MatrixEvent {
|
||||
return this.lastEvent;
|
||||
return this.lastEvent ?? this.lastReply();
|
||||
}
|
||||
|
||||
public get events(): MatrixEvent[] {
|
||||
@ -338,7 +367,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
return this.timelineSet.getLiveTimeline();
|
||||
}
|
||||
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, direction: Direction.Backward }): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
@ -370,7 +399,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
return this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
|
||||
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
|
||||
const prependEvents = (opts.direction ?? Direction.Backward) === Direction.Backward;
|
||||
|
||||
this.timelineSet.addEventsToTimeline(
|
||||
events,
|
||||
|
Reference in New Issue
Block a user