diff --git a/spec/integ/matrix-client-retrying.spec.js b/spec/integ/matrix-client-retrying.spec.ts similarity index 91% rename from spec/integ/matrix-client-retrying.spec.js rename to spec/integ/matrix-client-retrying.spec.ts index 99f99f7dd..99214ca62 100644 --- a/spec/integ/matrix-client-retrying.spec.js +++ b/spec/integ/matrix-client-retrying.spec.ts @@ -23,7 +23,7 @@ describe("MatrixClient retrying", function() { ); httpBackend = testClient.httpBackend; client = testClient.client; - room = new Room(roomId); + room = new Room(roomId, client, userId); client.store.storeRoom(room); }); @@ -50,7 +50,10 @@ describe("MatrixClient retrying", function() { it("should mark events as EventStatus.CANCELLED when cancelled", function() { // send a couple of events; the second will be queued - const p1 = client.sendMessage(roomId, "m1").then(function(ev) { + const p1 = client.sendMessage(roomId, { + "msgtype": "m.text", + "body": "m1", + }).then(function(ev) { // we expect the first message to fail throw new Error('Message 1 unexpectedly sent successfully'); }, (e) => { @@ -60,7 +63,10 @@ describe("MatrixClient retrying", function() { // XXX: it turns out that the promise returned by this message // never gets resolved. // https://github.com/matrix-org/matrix-js-sdk/issues/496 - client.sendMessage(roomId, "m2"); + client.sendMessage(roomId, { + "msgtype": "m.text", + "body": "m2", + }); // both events should be in the timeline at this point const tl = room.getLiveTimeline().getEvents(); @@ -88,7 +94,7 @@ describe("MatrixClient retrying", function() { }).respond(400); // fail the first message // wait for the localecho of ev1 to be updated - const p3 = new Promise((resolve, reject) => { + const p3 = new Promise((resolve, reject) => { room.on("Room.localEchoUpdated", (ev0) => { if (ev0 === ev1) { resolve(); diff --git a/src/client.ts b/src/client.ts index ff6fd66fd..09e96624c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -689,6 +689,12 @@ interface IRoomsKeysResponse { } /* eslint-enable camelcase */ +// We're using this constant for methods overloading and inspect whether a variable +// contains an eventId or not. This was required to ensure backwards compatibility +// of methods for threads +// Probably not the most graceful solution but does a good enough job for now +const EVENT_ID_PREFIX = "$"; + /** * Represents a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used @@ -3392,10 +3398,11 @@ export class MatrixClient extends EventEmitter { /** * @param {string} roomId + * @param {string} threadId * @param {string} eventType * @param {Object} content * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to an empty object {} * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3405,20 +3412,45 @@ export class MatrixClient extends EventEmitter { content: IContent, txnId?: string, callback?: Callback, + ); + public sendEvent( + roomId: string, + threadId: string | null, + eventType: string, + content: IContent, + txnId?: string, + callback?: Callback, + ) + public sendEvent( + roomId: string, + threadId: string | null, + eventType: string | IContent, + content: IContent | string, + txnId?: string | Callback, + callback?: Callback, ): Promise { - return this.sendCompleteEvent(roomId, { type: eventType, content }, txnId, callback); + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = txnId as Callback; + txnId = content as string; + content = eventType as IContent; + eventType = threadId; + threadId = null; + } + return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId as string, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to an empty object {} * @return {module:http-api.MatrixError} Rejects: with an error response. */ private sendCompleteEvent( roomId: string, + threadId: string | null, eventObject: any, txnId?: string, callback?: Callback, @@ -3444,6 +3476,10 @@ export class MatrixClient extends EventEmitter { })); const room = this.getRoom(roomId); + const thread = room?.threads.get(threadId); + if (thread) { + localEvent.setThread(thread); + } // if this is a relation or redaction of an event // that hasn't been sent yet (e.g. with a local id starting with a ~) @@ -3460,12 +3496,12 @@ export class MatrixClient extends EventEmitter { const type = localEvent.getType(); logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`); - localEvent.setTxnId(txnId); + localEvent.setTxnId(txnId as string); localEvent.setStatus(EventStatus.SENDING); // add this event immediately to the local store as 'sending'. if (room) { - room.addPendingEvent(localEvent, txnId); + room.addPendingEvent(localEvent, txnId as string); } // addPendingEvent can change the state to NOT_SENT if it believes @@ -3655,7 +3691,7 @@ export class MatrixClient extends EventEmitter { * supplied. * @param {object|module:client.callback} cbOrOpts * Options to pass on, may contain `reason`. - * Can be callback for backwards compatibility. + * Can be callback for backwards compatibility. Deprecated * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3664,22 +3700,43 @@ export class MatrixClient extends EventEmitter { eventId: string, txnId?: string, cbOrOpts?: Callback | IRedactOpts, + ); + public redactEvent( + roomId: string, + threadId: string | null, + eventId: string, + txnId?: string, + cbOrOpts?: Callback | IRedactOpts, + ); + public redactEvent( + roomId: string, + threadId: string | null, + eventId: string, + txnId?: string | Callback | IRedactOpts, + cbOrOpts?: Callback | IRedactOpts, ): Promise { + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + cbOrOpts = txnId as (Callback | IRedactOpts); + txnId = eventId; + eventId = threadId; + threadId = null; + } const opts = typeof (cbOrOpts) === 'object' ? cbOrOpts : {}; const reason = opts.reason; const callback = typeof (cbOrOpts) === 'function' ? cbOrOpts : undefined; - return this.sendCompleteEvent(roomId, { + return this.sendCompleteEvent(roomId, threadId, { type: EventType.RoomRedaction, content: { reason: reason }, redacts: eventId, - }, txnId, callback); + }, txnId as string, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {Object} content * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to an ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3688,19 +3745,47 @@ export class MatrixClient extends EventEmitter { content: IContent, txnId?: string, callback?: Callback, + ) + public sendMessage( + roomId: string, + threadId: string | null, + content: IContent, + txnId?: string, + callback?: Callback, + ) + public sendMessage( + roomId: string, + threadId: string | null | IContent, + content: IContent | string, + txnId?: string | Callback, + callback?: Callback, ): Promise { + if (typeof threadId !== "string" && threadId !== null) { + callback = txnId as Callback; + txnId = content as string; + content = threadId as IContent; + threadId = null; + } if (utils.isFunction(txnId)) { callback = txnId as any as Callback; // for legacy txnId = undefined; } - return this.sendEvent(roomId, EventType.RoomMessage, content, txnId, callback); + return this.sendEvent( + roomId, + threadId as (string | null), + EventType.RoomMessage, + content as IContent, + txnId as string, + callback, + ); } /** * @param {string} roomId + * @param {string} threadId * @param {string} body * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to an empty object {} * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3709,29 +3794,76 @@ export class MatrixClient extends EventEmitter { body: string, txnId?: string, callback?: Callback, + ) + public sendTextMessage( + roomId: string, + threadId: string | null, + body: string, + txnId?: string, + callback?: Callback, + ) + public sendTextMessage( + roomId: string, + threadId: string | null, + body: string, + txnId?: string | Callback, + callback?: Callback, ): Promise { + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = txnId as Callback; + txnId = body; + body = threadId; + threadId = null; + } const content = ContentHelpers.makeTextMessage(body); - return this.sendMessage(roomId, content, txnId, callback); + return this.sendMessage(roomId, threadId, content, txnId as string, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {string} body * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public sendNotice(roomId: string, body: string, txnId?: string, callback?: Callback): Promise { + public sendNotice( + roomId: string, + body: string, + txnId?: string, + callback?: Callback, + ) + public sendNotice( + roomId: string, + threadId: string | null, + body: string, + txnId?: string, + callback?: Callback, + ); + public sendNotice( + roomId: string, + threadId: string | null, + body: string, + txnId?: string | Callback, + callback?: Callback, + ): Promise { + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = txnId as Callback; + txnId = body; + body = threadId; + threadId = null; + } const content = ContentHelpers.makeNotice(body); - return this.sendMessage(roomId, content, txnId, callback); + return this.sendMessage(roomId, threadId, content, txnId as string, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {string} body * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3740,17 +3872,38 @@ export class MatrixClient extends EventEmitter { body: string, txnId?: string, callback?: Callback, + ) + public sendEmoteMessage( + roomId: string, + threadId: string | null, + body: string, + txnId?: string, + callback?: Callback, + ); + public sendEmoteMessage( + roomId: string, + threadId: string | null, + body: string, + txnId?: string | Callback, + callback?: Callback, ): Promise { + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = txnId as Callback; + txnId = body; + body = threadId; + threadId = null; + } const content = ContentHelpers.makeEmoteMessage(body); - return this.sendMessage(roomId, content, txnId, callback); + return this.sendMessage(roomId, threadId, content, txnId as string, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {string} url * @param {Object} info * @param {string} text - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3758,9 +3911,32 @@ export class MatrixClient extends EventEmitter { roomId: string, url: string, info?: IImageInfo, - text = "Image", + text?: string, + callback?: Callback, + ); + public sendImageMessage( + roomId: string, + threadId: string | null, + url: string, + info?: IImageInfo, + text?: string, + callback?: Callback, + ); + public sendImageMessage( + roomId: string, + threadId: string | null, + url: string | IImageInfo, + info?: IImageInfo | string, + text: Callback | string = "Image", callback?: Callback, ): Promise { + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = text as Callback; + text = info as string || "Image"; + info = url as IImageInfo; + url = threadId as string; + threadId = null; + } if (utils.isFunction(text)) { callback = text as any as Callback; // legacy text = undefined; @@ -3771,15 +3947,16 @@ export class MatrixClient extends EventEmitter { info: info, body: text, }; - return this.sendMessage(roomId, content, undefined, callback); + return this.sendMessage(roomId, threadId, content, undefined, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {string} url * @param {Object} info * @param {string} text - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3787,9 +3964,32 @@ export class MatrixClient extends EventEmitter { roomId: string, url: string, info?: IImageInfo, - text = "Sticker", + text?: string, + callback?: Callback, + ); + public sendStickerMessage( + roomId: string, + threadId: string | null, + url: string, + info?: IImageInfo, + text?: string, + callback?: Callback, + ); + public sendStickerMessage( + roomId: string, + threadId: string | null, + url: string | IImageInfo, + info?: IImageInfo | string, + text: Callback | string = "Sticker", callback?: Callback, ): Promise { + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = text as Callback; + text = info as string || "Sticker"; + info = url as IImageInfo; + url = threadId as string; + threadId = null; + } if (utils.isFunction(text)) { callback = text as any as Callback; // legacy text = undefined; @@ -3799,14 +3999,15 @@ export class MatrixClient extends EventEmitter { info: info, body: text, }; - return this.sendEvent(roomId, EventType.Sticker, content, undefined, callback); + return this.sendEvent(roomId, threadId, EventType.Sticker, content, undefined, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {string} body * @param {string} htmlBody - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3815,16 +4016,36 @@ export class MatrixClient extends EventEmitter { body: string, htmlBody: string, callback?: Callback, + ); + public sendHtmlMessage( + roomId: string, + threadId: string | null, + body: string, + htmlBody: string, + callback?: Callback, + ) + public sendHtmlMessage( + roomId: string, + threadId: string | null, + body: string, + htmlBody: string | Callback, + callback?: Callback, ): Promise { - const content = ContentHelpers.makeHtmlMessage(body, htmlBody); - return this.sendMessage(roomId, content, undefined, callback); + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = htmlBody as Callback; + htmlBody = body as string; + body = threadId; + threadId = null; + } + const content = ContentHelpers.makeHtmlMessage(body, htmlBody as string); + return this.sendMessage(roomId, threadId, content, undefined, callback); } /** * @param {string} roomId * @param {string} body * @param {string} htmlBody - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3833,16 +4054,37 @@ export class MatrixClient extends EventEmitter { body: string, htmlBody: string, callback?: Callback, + ); + public sendHtmlNotice( + roomId: string, + threadId: string | null, + body: string, + htmlBody: string, + callback?: Callback, + ) + public sendHtmlNotice( + roomId: string, + threadId: string | null, + body: string, + htmlBody: string | Callback, + callback?: Callback, ): Promise { - const content = ContentHelpers.makeHtmlNotice(body, htmlBody); - return this.sendMessage(roomId, content, undefined, callback); + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = htmlBody as Callback; + htmlBody = body as string; + body = threadId; + threadId = null; + } + const content = ContentHelpers.makeHtmlNotice(body, htmlBody as string); + return this.sendMessage(roomId, threadId, content, undefined, callback); } /** * @param {string} roomId + * @param {string} threadId * @param {string} body * @param {string} htmlBody - * @param {module:client.callback} callback Optional. + * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3851,9 +4093,29 @@ export class MatrixClient extends EventEmitter { body: string, htmlBody: string, callback?: Callback, + ); + public sendHtmlEmote( + roomId: string, + threadId: string | null, + body: string, + htmlBody: string, + callback?: Callback, + ) + public sendHtmlEmote( + roomId: string, + threadId: string | null, + body: string, + htmlBody: string | Callback, + callback?: Callback, ): Promise { - const content = ContentHelpers.makeHtmlEmote(body, htmlBody); - return this.sendMessage(roomId, content, undefined, callback); + if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { + callback = htmlBody as Callback; + htmlBody = body as string; + body = threadId; + threadId = null; + } + const content = ContentHelpers.makeHtmlEmote(body, htmlBody as string); + return this.sendMessage(roomId, threadId, content, undefined, callback); } /** diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 10cfb8d5d..1897d6741 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -28,6 +28,7 @@ import { Room } from "./room"; import { Filter } from "../filter"; import { EventType, RelationType } from "../@types/event"; import { RoomState } from "./room-state"; +import { Thread } from "./thread"; // var DEBUG = false; const DEBUG = true; @@ -153,18 +154,18 @@ export class EventTimelineSet extends EventEmitter { * * @throws If opts.pendingEventOrdering was not 'detached' */ - public getPendingEvents(): MatrixEvent[] { + public getPendingEvents(thread?: Thread): MatrixEvent[] { if (!this.room || !this.displayPendingEvents) { return []; } + const pendingEvents = this.room.getPendingEvents(thread); if (this.filter) { - return this.filter.filterRoomTimeline(this.room.getPendingEvents()); + return this.filter.filterRoomTimeline(pendingEvents); } else { - return this.room.getPendingEvents(); + return pendingEvents; } } - /** * Get the live timeline for this room. * diff --git a/src/models/room.ts b/src/models/room.ts index 324ae8cef..abd74b527 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -225,12 +225,6 @@ export class Room extends EventEmitter { this.reEmitter = new ReEmitter(this); opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological; - if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) { - throw new Error( - "opts.pendingEventOrdering MUST be either 'chronological' or " + - "'detached'. Got: '" + opts.pendingEventOrdering + "'", - ); - } this.name = roomId; @@ -241,7 +235,7 @@ export class Room extends EventEmitter { this.fixUpLegacyTimelineFields(); - if (this.opts.pendingEventOrdering == "detached") { + if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) { this.pendingEventList = []; const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId)); if (serializedPendingEventList) { @@ -452,14 +446,16 @@ export class Room extends EventEmitter { * * @throws If opts.pendingEventOrdering was not 'detached' */ - public getPendingEvents(): MatrixEvent[] { - if (this.opts.pendingEventOrdering !== "detached") { + public getPendingEvents(thread?: Thread): MatrixEvent[] { + if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) { throw new Error( "Cannot call getPendingEvents with pendingEventOrdering == " + this.opts.pendingEventOrdering); } - return this.pendingEventList; + return this.pendingEventList.filter(event => { + return !thread || thread.id === event.threadRootId; + }); } /** @@ -469,7 +465,7 @@ export class Room extends EventEmitter { * @return {boolean} True if an element was removed. */ public removePendingEvent(eventId: string): boolean { - if (this.opts.pendingEventOrdering !== "detached") { + if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) { throw new Error( "Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering); @@ -495,7 +491,7 @@ export class Room extends EventEmitter { * @return {boolean} */ public hasPendingEvent(eventId: string): boolean { - if (this.opts.pendingEventOrdering !== "detached") { + if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) { return false; } @@ -509,7 +505,7 @@ export class Room extends EventEmitter { * @return {MatrixEvent} */ public getPendingEvent(eventId: string): MatrixEvent | null { - if (this.opts.pendingEventOrdering !== "detached") { + if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) { return null; } @@ -856,7 +852,13 @@ export class Room extends EventEmitter { * the given event, or null if unknown */ public getTimelineForEvent(eventId: string): EventTimeline { - return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); + const event = this.findEventById(eventId); + const thread = this.findThreadForEvent(event); + if (thread) { + return thread.timelineSet.getLiveTimeline(); + } else { + return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); + } } /** @@ -1403,13 +1405,6 @@ export class Room extends EventEmitter { * unique transaction id. */ public addPendingEvent(event: MatrixEvent, txnId: string): void { - // TODO: Enable "pending events" for threads - // There's a fair few things to update to make them work with Threads - // Will get back to it when the plan is to build a more polished UI ready for production - if (this.client?.supportsExperimentalThreads() && event.threadRootId) { - return; - } - if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) { throw new Error("addPendingEvent called on an event with status " + event.status); @@ -1426,8 +1421,8 @@ export class Room extends EventEmitter { EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false); this.txnToEvent[txnId] = event; - - if (this.opts.pendingEventOrdering == "detached") { + const thread = this.threads.get(event.threadRootId); + if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) { if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) { logger.warn("Setting event as NOT_SENT due to messages in the same state"); event.setStatus(EventStatus.NOT_SENT); @@ -1446,7 +1441,7 @@ export class Room extends EventEmitter { let redactedEvent = this.pendingEventList && this.pendingEventList.find(e => e.getId() === redactId); if (!redactedEvent) { - redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); + redactedEvent = this.findEventById(redactId); } if (redactedEvent) { redactedEvent.markLocallyRedacted(event); @@ -1454,16 +1449,21 @@ export class Room extends EventEmitter { } } } else { - for (let i = 0; i < this.timelineSets.length; i++) { - const timelineSet = this.timelineSets[i]; - if (timelineSet.getFilter()) { - if (timelineSet.getFilter().filterRoomTimeline([event]).length) { + if (thread) { + thread.timelineSet.addEventToTimeline(event, + thread.timelineSet.getLiveTimeline(), false); + } else { + for (let i = 0; i < this.timelineSets.length; i++) { + const timelineSet = this.timelineSets[i]; + if (timelineSet.getFilter()) { + if (timelineSet.getFilter().filterRoomTimeline([event]).length) { + timelineSet.addEventToTimeline(event, + timelineSet.getLiveTimeline(), false); + } + } else { timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), false); } - } else { - timelineSet.addEventToTimeline(event, - timelineSet.getLiveTimeline(), false); } } } @@ -1521,16 +1521,21 @@ export class Room extends EventEmitter { * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated. */ private aggregateNonLiveRelation(event: MatrixEvent): void { - // TODO: We should consider whether this means it would be a better - // design to lift the relations handling up to the room instead. - for (let i = 0; i < this.timelineSets.length; i++) { - const timelineSet = this.timelineSets[i]; - if (timelineSet.getFilter()) { - if (timelineSet.getFilter().filterRoomTimeline([event]).length) { + const thread = this.findThreadForEvent(event); + if (thread) { + thread.timelineSet.aggregateRelations(event); + } else { + // TODO: We should consider whether this means it would be a better + // design to lift the relations handling up to the room instead. + for (let i = 0; i < this.timelineSets.length; i++) { + const timelineSet = this.timelineSets[i]; + if (timelineSet.getFilter()) { + if (timelineSet.getFilter().filterRoomTimeline([event]).length) { + timelineSet.aggregateRelations(event); + } + } else { timelineSet.aggregateRelations(event); } - } else { - timelineSet.aggregateRelations(event); } } } @@ -1571,11 +1576,16 @@ export class Room extends EventEmitter { // any, which is good, because we don't want to try decoding it again). localEvent.handleRemoteEcho(remoteEvent.event); - for (let i = 0; i < this.timelineSets.length; i++) { - const timelineSet = this.timelineSets[i]; + const thread = this.threads.get(remoteEvent.threadRootId); + if (thread) { + thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); + } else { + for (let i = 0; i < this.timelineSets.length; i++) { + const timelineSet = this.timelineSets[i]; - // if it's already in the timeline, update the timeline map. If it's not, add it. - timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); + // if it's already in the timeline, update the timeline map. If it's not, add it. + timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); + } } this.emit("Room.localEchoUpdated", localEvent, this, @@ -1608,7 +1618,7 @@ export class Room extends EventEmitter { // SENT races against /sync, so we have to special-case it. if (newStatus == EventStatus.SENT) { - const timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId); + const timeline = this.getTimelineForEvent(newEventId); if (timeline) { // we've already received the event via the event stream. // nothing more to do here. @@ -1636,11 +1646,16 @@ export class Room extends EventEmitter { // update the event id event.replaceLocalEventId(newEventId); - // if the event was already in the timeline (which will be the case if - // opts.pendingEventOrdering==chronological), we need to update the - // timeline map. - for (let i = 0; i < this.timelineSets.length; i++) { - this.timelineSets[i].replaceEventId(oldEventId, newEventId); + const thread = this.findThreadForEvent(event); + if (thread) { + thread.timelineSet.replaceEventId(oldEventId, newEventId); + } else { + // if the event was already in the timeline (which will be the case if + // opts.pendingEventOrdering==chronological), we need to update the + // timeline map. + for (let i = 0; i < this.timelineSets.length; i++) { + this.timelineSets[i].replaceEventId(oldEventId, newEventId); + } } } else if (newStatus == EventStatus.CANCELLED) { // remove it from the pending event list, or the timeline. diff --git a/src/models/thread.ts b/src/models/thread.ts index 5167a6b79..a47b8fefe 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -55,11 +55,20 @@ export class Thread extends TypedEventEmitter { this.timelineSet = new EventTimelineSet(this.room, { unstableClientRelationAggregation: true, timelineSupport: true, - pendingEvents: false, + pendingEvents: true, }); events.forEach(event => this.addEvent(event)); + + room.on("Room.localEchoUpdated", this.onEcho); + room.on("Room.timeline", this.onEcho); } + onEcho = (event: MatrixEvent) => { + if (this.timelineSet.eventIdToTimeline(event.getId())) { + this.emit(ThreadEvent.Update, this); + } + }; + /** * Add an event to the thread and updates * the tail/root references if needed