diff --git a/spec/unit/event-timeline.spec.ts b/spec/unit/event-timeline.spec.ts index abef5472db6..52ef23c3625 100644 --- a/spec/unit/event-timeline.spec.ts +++ b/spec/unit/event-timeline.spec.ts @@ -69,7 +69,6 @@ describe("EventTimeline", function () { const timelineStartState = timeline.startState!; expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, { timelineWasEmpty: undefined, - toStartOfTimeline: true, }); // @ts-ignore private prop const timelineEndState = timeline.endState!; @@ -314,11 +313,9 @@ describe("EventTimeline", function () { expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined, - toStartOfTimeline: false, }); expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined, - toStartOfTimeline: false, }); expect(events[0].forwardLooking).toBe(true); @@ -355,11 +352,9 @@ describe("EventTimeline", function () { expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined, - toStartOfTimeline: true, }); expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined, - toStartOfTimeline: true, }); expect(events[0].forwardLooking).toBe(false); diff --git a/spec/unit/room-state.spec.ts b/spec/unit/room-state.spec.ts index 20bc60b8bec..2c79440df71 100644 --- a/spec/unit/room-state.spec.ts +++ b/spec/unit/room-state.spec.ts @@ -1125,6 +1125,9 @@ describe("RoomState", function () { describe("skipWrongOrderRoomStateInserts", () => { const idNow = "now"; const idBefore = "before"; + const endState = new RoomState(roomId); + const startState = new RoomState(roomId, undefined, true); + let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void; const evNow = new MatrixEvent({ type: "m.call.member", @@ -1154,20 +1157,22 @@ describe("RoomState", function () { updatedStateWithBefore(); } }; - state.on(RoomStateEvent.Events, onRoomStateEvent); + endState.on(RoomStateEvent.Events, onRoomStateEvent); + startState.on(RoomStateEvent.Events, onRoomStateEvent); }); afterEach(() => { - state.off(RoomStateEvent.Events, onRoomStateEvent); + endState.off(RoomStateEvent.Events, onRoomStateEvent); + startState.off(RoomStateEvent.Events, onRoomStateEvent); updatedStateWithNow.mockReset(); updatedStateWithBefore.mockReset(); }); it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => { - state.setStateEvents([evNow, evBefore], { toStartOfTimeline: false }); + endState.setStateEvents([evNow, evBefore]); expect(updatedStateWithBefore).not.toHaveBeenCalled(); expect(updatedStateWithNow).toHaveBeenCalled(); }); it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => { - state.setStateEvents([evBefore, evNow], { toStartOfTimeline: true }); + startState.setStateEvents([evBefore, evNow]); expect(updatedStateWithBefore).toHaveBeenCalled(); expect(updatedStateWithNow).not.toHaveBeenCalled(); }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 7a550feda12..eb90b8bdfa0 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -406,14 +406,8 @@ describe("Room", function () { }), ]; await room.addLiveEvents(events); - expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[0]], { - timelineWasEmpty: false, - toStartOfTimeline: false, - }); - expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[1]], { - timelineWasEmpty: false, - toStartOfTimeline: false, - }); + expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: false }); + expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: false }); expect(events[0].forwardLooking).toBe(true); expect(events[1].forwardLooking).toBe(true); expect(room.oldState.setStateEvents).not.toHaveBeenCalled(); @@ -732,14 +726,8 @@ describe("Room", function () { ]; room.addEventsToTimeline(events, true, room.getLiveTimeline()); - expect(room.oldState.setStateEvents).toHaveBeenCalledWith([events[0]], { - timelineWasEmpty: undefined, - toStartOfTimeline: true, - }); - expect(room.oldState.setStateEvents).toHaveBeenCalledWith([events[1]], { - timelineWasEmpty: undefined, - toStartOfTimeline: true, - }); + expect(room.oldState.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined }); + expect(room.oldState.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined }); expect(events[0].forwardLooking).toBe(false); expect(events[1].forwardLooking).toBe(false); expect(room.currentState.setStateEvents).not.toHaveBeenCalled(); diff --git a/src/models/event-timeline.ts b/src/models/event-timeline.ts index 0429b5cee73..e8b84fd8068 100644 --- a/src/models/event-timeline.ts +++ b/src/models/event-timeline.ts @@ -127,7 +127,7 @@ export class EventTimeline { public constructor(private readonly eventTimelineSet: EventTimelineSet) { this.roomId = eventTimelineSet.room?.roomId ?? null; if (this.roomId) { - this.startState = new RoomState(this.roomId); + this.startState = new RoomState(this.roomId, undefined, true); this.endState = new RoomState(this.roomId); } @@ -151,7 +151,7 @@ export class EventTimeline { throw new Error("Cannot initialise state after events are added"); } - this.startState?.setStateEvents(stateEvents, { timelineWasEmpty, toStartOfTimeline: true }); + this.startState?.setStateEvents(stateEvents, { timelineWasEmpty }); this.endState?.setStateEvents(stateEvents, { timelineWasEmpty }); } @@ -375,7 +375,7 @@ export class EventTimeline { // modify state but only on unfiltered timelineSets if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { - roomState?.setStateEvents([event], { timelineWasEmpty, toStartOfTimeline }); + roomState?.setStateEvents([event], { timelineWasEmpty }); // it is possible that the act of setting the state event means we // can set more metadata (specifically sender/target props), so try // it again if the prop wasn't previously set. It may also mean that diff --git a/src/models/room-state.ts b/src/models/room-state.ts index ce26e5335d0..4eda87126c0 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -43,16 +43,7 @@ export interface IMarkerFoundOptions { */ timelineWasEmpty?: boolean; } -export interface ISetStateEventsOptions { - /** Whether the event is inserted at the start of the timeline - * or at the end. This is relevant for doing a sanity check: - * "Is the event id of the added event the same as the replaces_state id of the current event" - * We need to do this because sync sometimes feeds previous state events. - * If set to true the sanity check is inverted - * "Is the event id of the current event the same as the replaces_state id of the added event" - */ - toStartOfTimeline?: boolean; -} + // possible statuses for out-of-band member loading enum OobStatus { NotStarted, @@ -175,6 +166,7 @@ export class RoomState extends TypedEventEmitter public readonly beacons = new Map(); private _liveBeaconIds: BeaconIdentifier[] = []; + private _isStartTimelineState = false; /** * Construct room state. @@ -207,8 +199,10 @@ export class RoomState extends TypedEventEmitter public constructor( public readonly roomId: string, private oobMemberFlags = { status: OobStatus.NotStarted }, + isStartTimelineState: boolean = false, ) { super(); + this._isStartTimelineState = isStartTimelineState; this.updateModifiedTime(); } @@ -345,6 +339,21 @@ export class RoomState extends TypedEventEmitter return this._liveBeaconIds; } + /** + * This state is marked as a start state. This is used to skip state insertions that are + * in the wrong order. The order is determined by the `replaces_state` id. + * + * Example: + * A current state events `replaces_state` value is `1`. + * Trying to insert a state event with `event_id` `1` in its place would fail if isStartTimelineState = false. + * + * A current state events `event_id` is `2`. + * Trying to insert a state event where its `replaces_state` value is `2` would fail if isStartTimelineState = true. + */ + public get isStartTimelineState(): boolean { + return this._isStartTimelineState; + } + /** * Creates a copy of this room state so that mutations to either won't affect the other. * @returns the copy of the room state @@ -417,7 +426,7 @@ export class RoomState extends TypedEventEmitter * Fires {@link RoomStateEvent.Events} * Fires {@link RoomStateEvent.Marker} */ - public setStateEvents(stateEvents: MatrixEvent[], options?: IMarkerFoundOptions & ISetStateEventsOptions): void { + public setStateEvents(stateEvents: MatrixEvent[], options?: IMarkerFoundOptions): void { this.updateModifiedTime(); // update the core event dict @@ -437,7 +446,7 @@ export class RoomState extends TypedEventEmitter const lastId = lastStateEvent?.event.event_id; const newReplaceId = event.event.unsigned?.replaces_state; const newId = event.event.event_id; - if (options?.toStartOfTimeline) { + if (this._isStartTimelineState) { // Add an event to the start of the timeline. Its replace id not be the same as the one of the current/last start state event. if (newReplaceId && newId && newReplaceId === lastId) return; } else {