diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index 42f4bca4d..e6c45fbd4 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.spec.ts @@ -16,14 +16,15 @@ limitations under the License. import * as utils from "../test-utils/test-utils"; import { + DuplicateStrategy, EventTimeline, EventTimelineSet, EventType, + Filter, MatrixClient, MatrixEvent, MatrixEventEvent, Room, - DuplicateStrategy, } from '../../src'; import { Thread } from "../../src/models/thread"; import { ReEmitter } from "../../src/ReEmitter"; @@ -291,4 +292,34 @@ describe('EventTimelineSet', () => { expect(eventTimelineSet.canContain(event)).toBeTruthy(); }); }); + + describe("handleRemoteEcho", () => { + it("should add to liveTimeline only if the event matches the filter", () => { + const filter = new Filter(client.getUserId()!, "test_filter"); + filter.setDefinition({ + room: { + timeline: { + types: [EventType.RoomMessage], + }, + }, + }); + const eventTimelineSet = new EventTimelineSet(room, { filter }, client); + + const roomMessageEvent = new MatrixEvent({ + type: EventType.RoomMessage, + content: { body: "test" }, + event_id: "!test1:server", + }); + eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId()); + expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent); + + const roomFilteredEvent = new MatrixEvent({ + type: "other_event_type", + content: { body: "test" }, + event_id: "!test2:server", + }); + eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId()); + expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent); + }); + }); }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 695bc1227..800415d2b 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -288,11 +288,11 @@ describe("Room", function() { room.addLiveEvents(events); expect(room.currentState.setStateEvents).toHaveBeenCalledWith( [events[0]], - { timelineWasEmpty: undefined }, + { timelineWasEmpty: false }, ); expect(room.currentState.setStateEvents).toHaveBeenCalledWith( [events[1]], - { timelineWasEmpty: undefined }, + { timelineWasEmpty: false }, ); expect(events[0].forwardLooking).toBe(true); expect(events[1].forwardLooking).toBe(true); @@ -426,6 +426,17 @@ describe("Room", function() { // but without the event ID matching we will still have the local event in pending events expect(room.getEventForTxnId(txnId)).toBeUndefined(); }); + + it("should correctly handle remote echoes from other devices", () => { + const remoteEvent = utils.mkMessage({ + room: roomId, user: userA, event: true, + }); + remoteEvent.event.unsigned = { transaction_id: "TXN_ID" }; + + // add the remoteEvent + room.addLiveEvents([remoteEvent]); + expect(room.timeline.length).toEqual(1); + }); }); describe('addEphemeralEvents', () => { diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 8d11f4a63..a951a399a 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -729,18 +729,10 @@ export class EventTimelineSet extends TypedEventEmitter } } } - - if (event.getUnsigned().transaction_id) { - const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id]; - if (existingEvent) { - // remote echo of an event we sent earlier - this.handleRemoteEcho(event, existingEvent); - } - } } /** @@ -1996,7 +1988,7 @@ export class Room extends TypedEventEmitter * "Room.timeline". * * @param {MatrixEvent} event Event to be added - * @param {IAddLiveEventOptions} options addLiveEvent options + * @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options * @fires module:client~MatrixClient#event:"Room.timeline" * @private */ @@ -2344,7 +2336,7 @@ export class Room extends TypedEventEmitter fromCache = false, ): void { let duplicateStrategy = duplicateStrategyOrOpts as DuplicateStrategy; - let timelineWasEmpty: boolean; + let timelineWasEmpty = false; if (typeof (duplicateStrategyOrOpts) === 'object') { ({ duplicateStrategy, @@ -2383,10 +2375,25 @@ export class Room extends TypedEventEmitter const threadRoots = this.findThreadRoots(events); const eventsByThread: { [threadId: string]: MatrixEvent[] } = {}; + const options: IAddLiveEventOptions = { + duplicateStrategy, + fromCache, + timelineWasEmpty, + }; + for (const event of events) { // TODO: We should have a filter to say "only add state event types X Y Z to the timeline". this.processLiveEvent(event); + if (event.getUnsigned().transaction_id) { + const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id!]; + if (existingEvent) { + // remote echo of an event we sent earlier + this.handleRemoteEcho(event, existingEvent); + continue; // we can skip adding the event to the timeline sets, it is already there + } + } + const { shouldLiveInRoom, shouldLiveInThread, @@ -2399,11 +2406,7 @@ export class Room extends TypedEventEmitter eventsByThread[threadId]?.push(event); if (shouldLiveInRoom) { - this.addLiveEvent(event, { - duplicateStrategy, - fromCache, - timelineWasEmpty, - }); + this.addLiveEvent(event, options); } }