1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Support MSC4222 state_after (#4487)

* WIP support for state_after

* Fix sliding sync sdk / embedded tests

* Allow both state & state_after to be undefined

Since it must have allowed state to be undefined previously: the test
had it as such.

* Fix limited sync handling

* Need to use state_after being undefined

if state can be undefined anyway

* Make sliding sync sdk tests pass

* Remove deprecated interfaces & backwards-compat code

* Remove useless assignment

* Use updates unstable prefix

* Clarify docs

* Remove additional semi-backwards compatible overload

* Update unstable prefixes

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test for MSC4222 behaviour

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add comments to explain why things work as they are.

* Fix sync accumulator for state_after sync handling

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)"

This reverts commit 957329b218.

* Fix Sync Accumulator toJSON putting start timeline state in state_after field

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test case

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Timo <toger5@hotmail.de>
This commit is contained in:
David Baker
2024-11-27 11:40:41 +00:00
committed by GitHub
parent 66f099b2e7
commit 5bcd26e506
32 changed files with 1343 additions and 735 deletions

View File

@ -1327,7 +1327,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const syncResponse = getSyncResponse(["@bob:xyz"]); const syncResponse = getSyncResponse(["@bob:xyz"]);
// Every 2 messages in the room, the session should be rotated // Every 2 messages in the room, the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = { syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
rotation_period_msgs: 2, rotation_period_msgs: 2,
}; };
@ -1383,7 +1383,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const oneHourInMs = 60 * 60 * 1000; const oneHourInMs = 60 * 60 * 1000;
// Every 1h the session should be rotated // Every 1h the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = { syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
rotation_period_ms: oneHourInMs, rotation_period_ms: oneHourInMs,
}; };

View File

@ -1144,7 +1144,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update); const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog // Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]); await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend httpBackend
.when( .when(
"GET", "GET",
@ -1155,7 +1155,7 @@ describe("MatrixClient event timelines", function () {
}); });
await flushHttp(prom); await flushHttp(prom);
// but while loading the metadata, a new reply has arrived // but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]); await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!; const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order // then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([ expect(thread.events.map((it) => it.getId())).toEqual([
@ -1247,7 +1247,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update); const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog // Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]); await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend httpBackend
.when( .when(
"GET", "GET",
@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
}); });
await flushHttp(prom); await flushHttp(prom);
// but while loading the metadata, a new reply has arrived // but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]); await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!; const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order // then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([ expect(thread.events.map((it) => it.getId())).toEqual([
@ -1560,7 +1560,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true; thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.NewReply); const prom = emitPromise(room, ThreadEvent.NewReply);
respondToEvent(THREAD_ROOT_UPDATED); respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY2]); await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
await httpBackend.flushAllExpected(); await httpBackend.flushAllExpected();
await prom; await prom;
expect(thread.length).toBe(2); expect(thread.length).toBe(2);
@ -1685,7 +1685,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true; thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.Update); const prom = emitPromise(room, ThreadEvent.Update);
respondToEvent(THREAD_ROOT_UPDATED); respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY_REACTION]); await room.addLiveEvents([THREAD_REPLY_REACTION], { addToState: false });
await httpBackend.flushAllExpected(); await httpBackend.flushAllExpected();
await prom; await prom;
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread expect(thread.length).toBe(1); // reactions don't count towards the length of a thread

View File

@ -168,14 +168,17 @@ describe("MatrixClient", function () {
type: "test", type: "test",
content: {}, content: {},
}); });
room.addLiveEvents([ room.addLiveEvents(
utils.mkMembership({ [
user: userId, utils.mkMembership({
room: roomId, user: userId,
mship: KnownMembership.Join, room: roomId,
event: true, mship: KnownMembership.Join,
}), event: true,
]); }),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests(); httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room); store.storeRoom(room);
@ -188,14 +191,17 @@ describe("MatrixClient", function () {
const roomId = "!roomId:server"; const roomId = "!roomId:server";
const roomAlias = "#my-fancy-room:server"; const roomAlias = "#my-fancy-room:server";
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
room.addLiveEvents([ room.addLiveEvents(
utils.mkMembership({ [
user: userId, utils.mkMembership({
room: roomId, user: userId,
mship: KnownMembership.Join, room: roomId,
event: true, mship: KnownMembership.Join,
}), event: true,
]); }),
],
{ addToState: true },
);
store.storeRoom(room); store.storeRoom(room);
// The method makes a request to resolve the alias // The method makes a request to resolve the alias
@ -275,14 +281,17 @@ describe("MatrixClient", function () {
content: {}, content: {},
}); });
room.addLiveEvents([ room.addLiveEvents(
utils.mkMembership({ [
user: userId, utils.mkMembership({
room: roomId, user: userId,
mship: KnownMembership.Knock, room: roomId,
event: true, mship: KnownMembership.Knock,
}), event: true,
]); }),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests(); httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room); store.storeRoom(room);

View File

@ -556,7 +556,7 @@ describe("MatrixClient syncing", () => {
}); });
it("should resolve incoming invites from /sync", () => { it("should resolve incoming invites from /sync", () => {
syncData.rooms.join[roomOne].state.events.push( syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({ utils.mkMembership({
room: roomOne, room: roomOne,
mship: KnownMembership.Invite, mship: KnownMembership.Invite,
@ -589,7 +589,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost", name: "The Ghost",
}) as IMinimalEvent, }) as IMinimalEvent,
]; ];
syncData.rooms.join[roomOne].state.events.push( syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({ utils.mkMembership({
room: roomOne, room: roomOne,
mship: KnownMembership.Invite, mship: KnownMembership.Invite,
@ -617,7 +617,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost", name: "The Ghost",
}) as IMinimalEvent, }) as IMinimalEvent,
]; ];
syncData.rooms.join[roomOne].state.events.push( syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({ utils.mkMembership({
room: roomOne, room: roomOne,
mship: KnownMembership.Invite, mship: KnownMembership.Invite,
@ -644,7 +644,7 @@ describe("MatrixClient syncing", () => {
}); });
it("should no-op if resolveInvitesToProfiles is not set", () => { it("should no-op if resolveInvitesToProfiles is not set", () => {
syncData.rooms.join[roomOne].state.events.push( syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({ utils.mkMembership({
room: roomOne, room: roomOne,
mship: KnownMembership.Invite, mship: KnownMembership.Invite,
@ -1373,6 +1373,114 @@ describe("MatrixClient syncing", () => {
expect(stateEventEmitCount).toEqual(2); expect(stateEventEmitCount).toEqual(2);
}); });
}); });
describe("msc4222", () => {
const roomOneSyncOne = {
"timeline": {
events: [
utils.mkMessage({
room: roomOne,
user: otherUserId,
msg: "hello",
}),
],
},
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: otherUserId,
content: {
name: "Initial room name",
},
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {},
}),
],
},
};
const roomOneSyncTwo = {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
it("should ignore state events in timeline when state_after is present", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
});
}); });
describe("timeline", () => { describe("timeline", () => {
@ -2274,6 +2382,57 @@ describe("MatrixClient syncing", () => {
}), }),
]); ]);
}); });
describe("msc4222", () => {
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("POST", "/filter").respond(200, {
filter_id: "another_id",
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: {
[roomOne]: {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
},
},
},
});
const [[room]] = await Promise.all([
client!.syncLeftRooms(),
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend!.flush("/filter").then(() => {
return httpBackend!.flushAllExpected();
}),
]);
expect(room.name).toEqual("Empty room");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
}); });
describe("peek", () => { describe("peek", () => {

View File

@ -128,7 +128,7 @@ describe("MatrixClient syncing", () => {
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] }); const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!; const threadReply = thread.events.at(-1)!;
await room.addLiveEvents([thread.rootEvent]); await room.addLiveEvents([thread.rootEvent], { addToState: false });
// Initialize read receipt datastructure before testing the reaction // Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false); room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);

View File

@ -601,13 +601,13 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true, initial: true,
name: "Room with Invite", name: "Room with Invite",
required_state: [], required_state: [
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""), mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId), mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee), mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
], ],
timeline: [],
}); });
await httpBackend!.flush("/profile", 1, 1000); await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name); await emitPromise(client!, RoomMemberEvent.Name);
@ -921,13 +921,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id"; const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing", name: "Room with typing",
required_state: [], required_state: [
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""), mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId), mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
], ],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true, initial: true,
}); });
await emitPromise(client!, ClientEvent.Room); await emitPromise(client!, ClientEvent.Room);
@ -962,13 +961,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id"; const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing", name: "Room with typing",
required_state: [], required_state: [
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""), mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId), mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
], ],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true, initial: true,
}); });
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;

View File

@ -86,7 +86,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
}; };
for (let i = 0; i < roomMembers.length; i++) { for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push( roomResponse.state!.events.push(
mkMembershipCustom({ mkMembershipCustom({
membership: KnownMembership.Join, membership: KnownMembership.Join,
sender: roomMembers[i], sender: roomMembers[i],

View File

@ -178,6 +178,6 @@ export const populateThread = ({
}: MakeThreadProps): MakeThreadResult => { }: MakeThreadProps): MakeThreadResult => {
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts }); const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
ret.thread.initialEventsFetched = true; ret.thread.initialEventsFetched = true;
room.addLiveEvents(ret.events); room.addLiveEvents(ret.events, { addToState: false });
return ret; return ret;
}; };

View File

@ -261,7 +261,7 @@ describe("RoomWidgetClient", () => {
expect(injectSpy).toHaveBeenCalled(); expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any; const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[2][0]; const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request"); expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId"); expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
}); });
@ -287,7 +287,7 @@ describe("RoomWidgetClient", () => {
expect(injectSpy).toHaveBeenCalled(); expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any; const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[2][0]; const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request"); expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId"); expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
}); });
@ -326,13 +326,13 @@ describe("RoomWidgetClient", () => {
// it has been called with the event sent by ourselves // it has been called with the event sent by ourselves
const call = injectSpy.mock.calls[0] as any; const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[2][0]; const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request"); expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId"); expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
// It has been called by the event we blocked because of our send right afterwards // It has been called by the event we blocked because of our send right afterwards
const call2 = injectSpy.mock.calls[1] as any; const call2 = injectSpy.mock.calls[1] as any;
const injectedEv2 = call2[2][0]; const injectedEv2 = call2[3][0];
expect(injectedEv2.getType()).toBe("org.matrix.rageshake_request"); expect(injectedEv2.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv2.getUnsigned().transaction_id).toBe("4567"); expect(injectedEv2.getUnsigned().transaction_id).toBe("4567");
}); });

View File

@ -74,7 +74,7 @@ describe("eventMapperFor", function () {
const event = mapper(eventDefinition); const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent); expect(event).toBeInstanceOf(MatrixEvent);
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event); expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition); const event2 = mapper(eventDefinition);
@ -109,7 +109,7 @@ describe("eventMapperFor", function () {
room.oldState.setStateEvents([event]); room.oldState.setStateEvents([event]);
room.currentState.setStateEvents([event]); room.currentState.setStateEvents([event]);
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event); expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition); const event2 = mapper(eventDefinition);

View File

@ -104,7 +104,7 @@ describe("EventTimelineSet", () => {
it("Adds event to the live timeline in the timeline set", () => { it("Adds event to the live timeline in the timeline set", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline(); const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent); eventTimelineSet.addLiveEvent(messageEvent, { addToState: false });
expect(liveTimeline.getEvents().length).toStrictEqual(1); expect(liveTimeline.getEvents().length).toStrictEqual(1);
}); });
@ -113,6 +113,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent, { eventTimelineSet.addLiveEvent(messageEvent, {
duplicateStrategy: DuplicateStrategy.Replace, duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
}); });
expect(liveTimeline.getEvents().length).toStrictEqual(1); expect(liveTimeline.getEvents().length).toStrictEqual(1);
@ -130,6 +131,7 @@ describe("EventTimelineSet", () => {
// replace. // replace.
eventTimelineSet.addLiveEvent(duplicateMessageEvent, { eventTimelineSet.addLiveEvent(duplicateMessageEvent, {
duplicateStrategy: DuplicateStrategy.Replace, duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
}); });
const eventsInLiveTimeline = liveTimeline.getEvents(); const eventsInLiveTimeline = liveTimeline.getEvents();
@ -144,6 +146,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, { eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true, toStartOfTimeline: true,
addToState: false,
}); });
expect(liveTimeline.getEvents().length).toStrictEqual(1); expect(liveTimeline.getEvents().length).toStrictEqual(1);
}); });
@ -151,10 +154,17 @@ describe("EventTimelineSet", () => {
it("Make sure legacy overload passing options directly as parameters still works", () => { it("Make sure legacy overload passing options directly as parameters still works", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline(); const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(() => { expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true); eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
}).not.toThrow(); }).not.toThrow();
expect(() => { expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false); eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
fromCache: false,
addToState: false,
});
}).not.toThrow(); }).not.toThrow();
}); });
@ -167,11 +177,13 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, { eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
toStartOfTimeline: true, toStartOfTimeline: true,
addToState: false,
}); });
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, { eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true, toStartOfTimeline: true,
addToState: false,
}); });
expect(liveTimeline.getEvents()).toHaveLength(1); expect(liveTimeline.getEvents()).toHaveLength(1);
const [event] = liveTimeline.getEvents(); const [event] = liveTimeline.getEvents();
@ -202,6 +214,7 @@ describe("EventTimelineSet", () => {
expect(() => { expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, { eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true, toStartOfTimeline: true,
addToState: false,
}); });
}).toThrow(); }).toThrow();
}); });
@ -214,6 +227,7 @@ describe("EventTimelineSet", () => {
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, { eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
toStartOfTimeline: true, toStartOfTimeline: true,
addToState: false,
}); });
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
}); });
@ -232,6 +246,7 @@ describe("EventTimelineSet", () => {
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, { eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
toStartOfTimeline: true, toStartOfTimeline: true,
addToState: false,
}); });
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
}); });
@ -248,6 +263,7 @@ describe("EventTimelineSet", () => {
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0); expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, { nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
toStartOfTimeline: true, toStartOfTimeline: true,
addToState: false,
}); });
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1); expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
}); });
@ -257,7 +273,7 @@ describe("EventTimelineSet", () => {
describe("aggregateRelations", () => { describe("aggregateRelations", () => {
describe("with unencrypted events", () => { describe("with unencrypted events", () => {
beforeEach(() => { beforeEach(() => {
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo"); eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
}); });
itShouldReturnTheRelatedEvents(); itShouldReturnTheRelatedEvents();
@ -279,7 +295,7 @@ describe("EventTimelineSet", () => {
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true); replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure"); replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo"); eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
}); });
it("should not return the related events", () => { it("should not return the related events", () => {

View File

@ -98,7 +98,7 @@ describe("EventTimeline", function () {
expect(function () { expect(function () {
timeline.initialiseState(state); timeline.initialiseState(state);
}).not.toThrow(); }).not.toThrow();
timeline.addEvent(event, { toStartOfTimeline: false }); timeline.addEvent(event, { toStartOfTimeline: false, addToState: false });
expect(function () { expect(function () {
timeline.initialiseState(state); timeline.initialiseState(state);
}).toThrow(); }).toThrow();
@ -182,9 +182,9 @@ describe("EventTimeline", function () {
]; ];
it("should be able to add events to the end", function () { it("should be able to add events to the end", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false }); timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
const initialIndex = timeline.getBaseIndex(); const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false }); timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex); expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2); expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]); expect(timeline.getEvents()[0]).toEqual(events[0]);
@ -192,9 +192,9 @@ describe("EventTimeline", function () {
}); });
it("should be able to add events to the start", function () { it("should be able to add events to the start", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true }); timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
const initialIndex = timeline.getBaseIndex(); const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: true }); timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1); expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2); expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]); expect(timeline.getEvents()[0]).toEqual(events[1]);
@ -238,9 +238,9 @@ describe("EventTimeline", function () {
content: { name: "Old Room Name" }, content: { name: "Old Room Name" },
}); });
timeline.addEvent(newEv, { toStartOfTimeline: false }); timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.sender).toEqual(sentinel); expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true }); timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.sender).toEqual(oldSentinel); expect(oldEv.sender).toEqual(oldSentinel);
}); });
@ -280,9 +280,9 @@ describe("EventTimeline", function () {
skey: userA, skey: userA,
event: true, event: true,
}); });
timeline.addEvent(newEv, { toStartOfTimeline: false }); timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.target).toEqual(sentinel); expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true }); timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.target).toEqual(oldSentinel); expect(oldEv.target).toEqual(oldSentinel);
}); });
@ -308,8 +308,8 @@ describe("EventTimeline", function () {
}), }),
]; ];
timeline.addEvent(events[0], { toStartOfTimeline: false }); timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: false }); timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: true });
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], { expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined, timelineWasEmpty: undefined,
@ -347,8 +347,8 @@ describe("EventTimeline", function () {
}), }),
]; ];
timeline.addEvent(events[0], { toStartOfTimeline: true }); timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: true }); timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: true });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], { expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined, timelineWasEmpty: undefined,
@ -365,11 +365,15 @@ describe("EventTimeline", function () {
); );
it("Make sure legacy overload passing options directly as parameters still works", () => { it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow(); expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false })).not.toThrow();
// @ts-ignore stateContext is not a valid param // @ts-ignore stateContext is not a valid param
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow(); expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
expect(() => expect(() =>
timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }), timeline.addEvent(events[0], {
toStartOfTimeline: false,
addToState: false,
roomState: new RoomState(roomId),
}),
).not.toThrow(); ).not.toThrow();
}); });
}); });
@ -397,8 +401,8 @@ describe("EventTimeline", function () {
]; ];
it("should remove events", function () { it("should remove events", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false }); timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: false }); timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(2); expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId()!); let ev = timeline.removeEvent(events[0].getId()!);
@ -411,9 +415,9 @@ describe("EventTimeline", function () {
}); });
it("should update baseIndex", function () { it("should update baseIndex", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false }); timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: true }); timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false }); timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(3); expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1); expect(timeline.getBaseIndex()).toEqual(1);
@ -430,11 +434,11 @@ describe("EventTimeline", function () {
// - removing the last event got baseIndex into such a state that // - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase. // further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event", function () { it("should not make baseIndex assplode when removing the last event", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true }); timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
timeline.removeEvent(events[0].getId()!); timeline.removeEvent(events[0].getId()!);
const initialIndex = timeline.getBaseIndex(); const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false }); timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false }); timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex); expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2); expect(timeline.getEvents().length).toEqual(2);
}); });

View File

@ -2799,24 +2799,28 @@ describe("MatrixClient", function () {
roomCreateEvent(room1.roomId, replacedByCreate1.roomId), roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId), predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId),
], ],
{}, { addToState: true },
); );
room2.addLiveEvents( room2.addLiveEvents(
[ [
roomCreateEvent(room2.roomId, replacedByCreate2.roomId), roomCreateEvent(room2.roomId, replacedByCreate2.roomId),
predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId), predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId),
], ],
{}, { addToState: true },
); );
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {}); replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {}); addToState: true,
});
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {
addToState: true,
});
replacedByDynamicPredecessor1.addLiveEvents( replacedByDynamicPredecessor1.addLiveEvents(
[tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)], [tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)],
{}, { addToState: true },
); );
replacedByDynamicPredecessor2.addLiveEvents( replacedByDynamicPredecessor2.addLiveEvents(
[tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)], [tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)],
{}, { addToState: true },
); );
return { return {
@ -2854,10 +2858,10 @@ describe("MatrixClient", function () {
const room2 = new Room("room2", client, "@daryl:alexandria.example.com"); const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
client.store = new StubStore(); client.store = new StubStore();
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2]; client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {}); room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {}); room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {}); replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {}); replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
// When we ask for the visible rooms // When we ask for the visible rooms
const rooms = client.getVisibleRooms(); const rooms = client.getVisibleRooms();
@ -2937,15 +2941,15 @@ describe("MatrixClient", function () {
const room4 = new Room("room4", client, "@michonne:hawthorne.example.com"); const room4 = new Room("room4", client, "@michonne:hawthorne.example.com");
if (creates) { if (creates) {
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]); room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true });
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]); room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)], { addToState: true });
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]); room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)], { addToState: true });
} }
if (tombstones) { if (tombstones) {
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {}); room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], { addToState: true });
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {}); room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {}); room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], { addToState: true });
} }
mocked(store.getRoom).mockImplementation((roomId: string) => { mocked(store.getRoom).mockImplementation((roomId: string) => {
@ -2980,17 +2984,17 @@ describe("MatrixClient", function () {
const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com"); const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com");
const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com"); const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com");
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {}); dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]); dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {}); dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]); room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {}); room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]); dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {}); dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]); dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
mocked(store.getRoom) mocked(store.getRoom)
.mockClear() .mockClear()

View File

@ -99,7 +99,7 @@ describe("MatrixEvent", () => {
const room = new Room("!roomid:e.xyz", mockClient, "myname"); const room = new Room("!roomid:e.xyz", mockClient, "myname");
const ev = createEvent("$event1:server"); const ev = createEvent("$event1:server");
await room.addLiveEvents([ev]); await room.addLiveEvents([ev], { addToState: false });
await room.createThreadsTimelineSets(); await room.createThreadsTimelineSets();
expect(ev.threadRootId).toBeUndefined(); expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]); expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
@ -120,7 +120,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server"); const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]); await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets(); await room.createThreadsTimelineSets();
expect(threadRoot.threadRootId).toEqual(threadRoot.getId()); expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@ -143,7 +143,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server"); const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]); await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets(); await room.createThreadsTimelineSets();
expect(ev.threadRootId).toEqual(threadRoot.getId()); expect(ev.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@ -167,7 +167,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const reaction = createReactionEvent("$reaction:server", ev.getId()!); const reaction = createReactionEvent("$reaction:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, reaction]); await room.addLiveEvents([threadRoot, ev, reaction], { addToState: false });
await room.createThreadsTimelineSets(); await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId()); expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@ -191,7 +191,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const edit = createEditEvent("$edit:server", ev.getId()!); const edit = createEditEvent("$edit:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, edit]); await room.addLiveEvents([threadRoot, ev, edit], { addToState: false });
await room.createThreadsTimelineSets(); await room.createThreadsTimelineSets();
expect(edit.threadRootId).toEqual(threadRoot.getId()); expect(edit.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@ -217,7 +217,7 @@ describe("MatrixEvent", () => {
const reply2 = createReplyEvent("$reply2:server", reply1.getId()!); const reply2 = createReplyEvent("$reply2:server", reply1.getId()!);
const reaction = createReactionEvent("$reaction:server", reply2.getId()!); const reaction = createReactionEvent("$reaction:server", reply2.getId()!);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]); await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction], { addToState: false });
await room.createThreadsTimelineSets(); await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId()); expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);

View File

@ -36,7 +36,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room // Given there are no receipts in the room
const room = createRoom(); const room = createRoom();
const [event] = createEvent(); const [event] = createEvent();
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
// When I ask about any event, then it is unread // When I ask about any event, then it is unread
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false); expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false);
@ -46,7 +46,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room // Given there are no receipts in the room
const room = createRoom(); const room = createRoom();
const [event] = createEventSentBy(readerId); const [event] = createEventSentBy(readerId);
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
// When I ask about an event I sent, it is read (because a synthetic // When I ask about an event I sent, it is read (because a synthetic
// receipt was created and stored in RoomReceipts) // receipt was created and stored in RoomReceipts)
@ -57,7 +57,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread // Given my event exists and is unread
const room = createRoom(); const room = createRoom();
const [event, eventId] = createEvent(); const [event, eventId] = createEvent();
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false); expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user // When we receive a receipt for this event+user
@ -72,7 +72,7 @@ describe("RoomReceipts", () => {
const room = createRoom(); const room = createRoom();
const [event1, event1Id] = createEvent(); const [event1, event1Id] = createEvent();
const [event2] = createEvent(); const [event2] = createEvent();
room.addLiveEvents([event1, event2]); room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the later event // When we receive a receipt for the later event
room.addReceipt(createReceipt(readerId, event2)); room.addReceipt(createReceipt(readerId, event2));
@ -86,7 +86,7 @@ describe("RoomReceipts", () => {
const room = createRoom(); const room = createRoom();
const [oldEvent, oldEventId] = createEvent(); const [oldEvent, oldEventId] = createEvent();
const [liveEvent] = createEvent(); const [liveEvent] = createEvent();
room.addLiveEvents([liveEvent]); room.addLiveEvents([liveEvent], { addToState: false });
createOldTimeline(room, [oldEvent]); createOldTimeline(room, [oldEvent]);
// When we receive a receipt for the live event // When we receive a receipt for the live event
@ -120,7 +120,7 @@ describe("RoomReceipts", () => {
const room = createRoom(); const room = createRoom();
const [event1] = createEvent(); const [event1] = createEvent();
const [event2, event2Id] = createEvent(); const [event2, event2Id] = createEvent();
room.addLiveEvents([event1, event2]); room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the earlier event // When we receive a receipt for the earlier event
room.addReceipt(createReceipt(readerId, event1)); room.addReceipt(createReceipt(readerId, event1));
@ -133,7 +133,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread // Given my event exists and is unread
const room = createRoom(); const room = createRoom();
const [event, eventId] = createEvent(); const [event, eventId] = createEvent();
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false); expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user // When we receive a receipt for another user
@ -151,7 +151,7 @@ describe("RoomReceipts", () => {
const room = createRoom(); const room = createRoom();
const [previousEvent] = createEvent(); const [previousEvent] = createEvent();
const [myEvent] = createEventSentBy(readerId); const [myEvent] = createEventSentBy(readerId);
room.addLiveEvents([previousEvent, myEvent]); room.addLiveEvents([previousEvent, myEvent], { addToState: false });
// And I just received a receipt for the previous event // And I just received a receipt for the previous event
room.addReceipt(createReceipt(readerId, previousEvent)); room.addReceipt(createReceipt(readerId, previousEvent));
@ -165,7 +165,7 @@ describe("RoomReceipts", () => {
const room = createRoom(); const room = createRoom();
const [myEvent] = createEventSentBy(readerId); const [myEvent] = createEventSentBy(readerId);
const [laterEvent] = createEvent(); const [laterEvent] = createEvent();
room.addLiveEvents([myEvent, laterEvent]); room.addLiveEvents([myEvent, laterEvent], { addToState: false });
// When I ask about the later event, it is unread (because it's after the synthetic receipt) // When I ask about the later event, it is unread (because it's after the synthetic receipt)
expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false); expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false);
@ -177,7 +177,7 @@ describe("RoomReceipts", () => {
const [event1] = createEvent(); const [event1] = createEvent();
const [event2, event2Id] = createEvent(); const [event2, event2Id] = createEvent();
const [event3, event3Id] = createEvent(); const [event3, event3Id] = createEvent();
room.addLiveEvents([event1, event2, event3]); room.addLiveEvents([event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order // When we receive receipts for the older events out of order
room.addReceipt(createReceipt(readerId, event2)); room.addReceipt(createReceipt(readerId, event2));
@ -192,7 +192,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread // Given my event exists and is unread
const room = createRoom(); const room = createRoom();
const [event, eventId] = createEvent(); const [event, eventId] = createEvent();
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false); expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user // When we receive a receipt for this event+user
@ -208,7 +208,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent(); const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root); const [event, eventId] = createThreadedEvent(root);
setupThread(room, root); setupThread(room, root);
room.addLiveEvents([root, event]); room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false); expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event on this thread // When we receive a receipt for this event on this thread
@ -225,7 +225,7 @@ describe("RoomReceipts", () => {
const [event1, event1Id] = createThreadedEvent(root); const [event1, event1Id] = createThreadedEvent(root);
const [event2] = createThreadedEvent(root); const [event2] = createThreadedEvent(root);
setupThread(room, root); setupThread(room, root);
room.addLiveEvents([root, event1, event2]); room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the later event // When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, event2, rootId)); room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@ -241,7 +241,7 @@ describe("RoomReceipts", () => {
const [event1] = createThreadedEvent(root); const [event1] = createThreadedEvent(root);
const [event2, event2Id] = createThreadedEvent(root); const [event2, event2Id] = createThreadedEvent(root);
setupThread(room, root); setupThread(room, root);
room.addLiveEvents([root, event1, event2]); room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the earlier event // When we receive a receipt for the earlier event
room.addReceipt(createThreadedReceipt(readerId, event1, rootId)); room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
@ -256,7 +256,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent(); const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root); const [event, eventId] = createThreadedEvent(root);
setupThread(room, root); setupThread(room, root);
room.addLiveEvents([root, event]); room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false); expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user // When we receive a receipt for another user
@ -278,7 +278,7 @@ describe("RoomReceipts", () => {
const [thread2] = createThreadedEvent(root2); const [thread2] = createThreadedEvent(root2);
setupThread(room, root1); setupThread(room, root1);
setupThread(room, root2); setupThread(room, root2);
room.addLiveEvents([root1, root2, thread1, thread2]); room.addLiveEvents([root1, root2, thread1, thread2], { addToState: false });
// When we receive a receipt for the later event // When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!)); room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!));
@ -295,7 +295,7 @@ describe("RoomReceipts", () => {
const [event2, event2Id] = createThreadedEvent(root); const [event2, event2Id] = createThreadedEvent(root);
const [event3, event3Id] = createThreadedEvent(root); const [event3, event3Id] = createThreadedEvent(root);
setupThread(room, root); setupThread(room, root);
room.addLiveEvents([root, event1, event2, event3]); room.addLiveEvents([root, event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order // When we receive receipts for the older events out of order
room.addReceipt(createThreadedReceipt(readerId, event2, rootId)); room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@ -329,7 +329,7 @@ describe("RoomReceipts", () => {
const [thread2b, thread2bId] = createThreadedEvent(main2); const [thread2b, thread2bId] = createThreadedEvent(main2);
setupThread(room, main1); setupThread(room, main1);
setupThread(room, main2); setupThread(room, main2);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b]); room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b], { addToState: false });
// And the timestamps on the events are consistent with the order above // And the timestamps on the events are consistent with the order above
main1.event.origin_server_ts = 1; main1.event.origin_server_ts = 1;
@ -377,7 +377,7 @@ describe("RoomReceipts", () => {
// Add the event to the room // Add the event to the room
// The receipt is removed from the dangling state // The receipt is removed from the dangling state
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
// Then the event is read // Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true); expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@ -398,7 +398,7 @@ describe("RoomReceipts", () => {
// Add the events to the room // Add the events to the room
// The receipt is removed from the dangling state // The receipt is removed from the dangling state
room.addLiveEvents([root, event]); room.addLiveEvents([root, event], { addToState: false });
// Then the event is read // Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true); expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@ -418,7 +418,7 @@ describe("RoomReceipts", () => {
// Add the event to the room // Add the event to the room
// The two receipts should be processed // The two receipts should be processed
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
// Then the event is read // Then the event is read
// We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId` // We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId`
@ -528,7 +528,7 @@ function createThreadedReceipt(userId: string, referencedEvent: MatrixEvent, thr
*/ */
function createOldTimeline(room: Room, events: MatrixEvent[]) { function createOldTimeline(room: Room, events: MatrixEvent[]) {
const oldTimeline = room.getUnfilteredTimelineSet().addTimeline(); const oldTimeline = room.getUnfilteredTimelineSet().addTimeline();
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, oldTimeline); room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, false, oldTimeline);
} }
/** /**

View File

@ -801,7 +801,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
// Ensure the root is in the room timeline // Ensure the root is in the room timeline
root.setThreadId(root.getId()); root.setThreadId(root.getId());
await room.addLiveEvents([root]); await room.addLiveEvents([root], { addToState: false });
// Create the thread and wait for it to be initialised // Create the thread and wait for it to be initialised
const thread = room.createThread(root.getId()!, root, [], false); const thread = room.createThread(root.getId()!, root, [], false);

View File

@ -106,7 +106,7 @@ describe("fixNotificationCountOnDecryption", () => {
mockClient, mockClient,
); );
room.addLiveEvents([event]); room.addLiveEvents([event], { addToState: false });
THREAD_ID = event.getId()!; THREAD_ID = event.getId()!;
threadEvent = mkEvent({ threadEvent = mkEvent({

View File

@ -198,8 +198,8 @@ describe("Relations", function () {
}); });
const timelineSet = new EventTimelineSet(room); const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(targetEvent); timelineSet.addLiveEvent(targetEvent, { addToState: false });
timelineSet.addLiveEvent(relationEvent); timelineSet.addLiveEvent(relationEvent, { addToState: false });
await relationsCreated; await relationsCreated;
} }
@ -212,8 +212,8 @@ describe("Relations", function () {
}); });
const timelineSet = new EventTimelineSet(room); const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(relationEvent); timelineSet.addLiveEvent(relationEvent, { addToState: false });
timelineSet.addLiveEvent(targetEvent); timelineSet.addLiveEvent(targetEvent, { addToState: false });
await relationsCreated; await relationsCreated;
} }

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,10 @@ limitations under the License.
import { ReceiptType } from "../../src/@types/read_receipts"; import { ReceiptType } from "../../src/@types/read_receipts";
import { import {
IJoinedRoom, Category,
IInvitedRoom, IInvitedRoom,
IInviteState,
IJoinedRoom,
IKnockedRoom, IKnockedRoom,
IKnockState, IKnockState,
ILeftRoom, ILeftRoom,
@ -27,7 +29,6 @@ import {
IStrippedState, IStrippedState,
ISyncResponse, ISyncResponse,
SyncAccumulator, SyncAccumulator,
IInviteState,
} from "../../src/sync-accumulator"; } from "../../src/sync-accumulator";
import { IRoomSummary } from "../../src"; import { IRoomSummary } from "../../src";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
@ -85,6 +86,7 @@ describe("SyncAccumulator", function () {
// technically cheating since we also cheekily pre-populate keys we // technically cheating since we also cheekily pre-populate keys we
// know that the sync accumulator will pre-populate. // know that the sync accumulator will pre-populate.
// It isn't 100% transitive. // It isn't 100% transitive.
const events = [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)];
const res = { const res = {
next_batch: "abc", next_batch: "abc",
rooms: { rooms: {
@ -92,18 +94,17 @@ describe("SyncAccumulator", function () {
leave: {}, leave: {},
join: { join: {
"!foo:bar": { "!foo:bar": {
account_data: { events: [] }, "account_data": { events: [] },
ephemeral: { events: [] }, "ephemeral": { events: [] },
unread_notifications: {}, "unread_notifications": {},
state: { "org.matrix.msc4222.state_after": { events },
events: [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)], "state": { events },
}, "summary": {
summary: {
"m.heroes": undefined, "m.heroes": undefined,
"m.joined_member_count": undefined, "m.joined_member_count": undefined,
"m.invited_member_count": undefined, "m.invited_member_count": undefined,
}, },
timeline: { "timeline": {
events: [msg("alice", "hi")], events: [msg("alice", "hi")],
prev_batch: "something", prev_batch: "something",
}, },
@ -882,6 +883,147 @@ describe("SyncAccumulator", function () {
).not.toBeUndefined(); ).not.toBeUndefined();
}); });
}); });
describe("msc4222", () => {
it("should accumulate state_after events", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 1",
},
skey: "",
}) as IStateEvent,
],
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).toEqual("Room 1");
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 2",
},
skey: "",
}) as IStateEvent,
],
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).toEqual("Room 2");
});
it("should ignore state events in timeline", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [],
},
"timeline": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 1",
},
skey: "",
}) as IStateEvent,
],
prev_batch: "something",
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).not.toEqual("Room 1");
});
it("should not rewind state_after to start of timeline in toJSON", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
"timeline": {
events: initState.events,
prev_batch: null,
},
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
const joinEvent = member("alice", KnownMembership.Join);
joinEvent.unsigned = { prev_content: initState.events[0].content, prev_sender: initState.events[0].sender };
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [joinEvent],
},
"timeline": {
events: [joinEvent],
prev_batch: "something",
},
}),
);
const roomData = sa.getJSON().roomsData[Category.Join]["!foo:bar"];
expect(roomData.state?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
KnownMembership.Knock,
);
expect(
roomData["org.matrix.msc4222.state_after"]?.events.find((e) => e.type === "m.room.member")?.content
.membership,
).toEqual(KnownMembership.Join);
expect(roomData.timeline?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
KnownMembership.Join,
);
});
});
}); });
function syncSkeleton( function syncSkeleton(
@ -961,5 +1103,6 @@ function member(localpart: string, membership: Membership) {
state_key: "@" + localpart + ":localhost", state_key: "@" + localpart + ":localhost",
sender: "@" + localpart + ":localhost", sender: "@" + localpart + ":localhost",
type: "m.room.member", type: "m.room.member",
unsigned: {},
}; };
} }

View File

@ -62,7 +62,7 @@ function addEventsToTimeline(timeline: EventTimeline, numEvents: number, toStart
user: USER_ID, user: USER_ID,
event: true, event: true,
}), }),
{ toStartOfTimeline }, { toStartOfTimeline, addToState: false },
); );
} }
} }
@ -451,8 +451,8 @@ describe("TimelineWindow", function () {
const liveEvents = createEvents(5); const liveEvents = createEvents(5);
const [, , e3, e4, e5] = oldEvents; const [, , e3, e4, e5] = oldEvents;
const [, e7, e8, e9, e10] = liveEvents; const [, e7, e8, e9, e10] = liveEvents;
room.addLiveEvents(liveEvents); room.addLiveEvents(liveEvents, { addToState: false });
room.addEventsToTimeline(oldEvents, true, oldTimeline); room.addEventsToTimeline(oldEvents, true, false, oldTimeline);
// And 2 windows over the timelines in this room // And 2 windows over the timelines in this room
const oldWindow = new TimelineWindow(mockClient, timelineSet); const oldWindow = new TimelineWindow(mockClient, timelineSet);

View File

@ -1566,16 +1566,19 @@ describe("Group Call", function () {
async (roomId, eventType, content, stateKey) => { async (roomId, eventType, content, stateKey) => {
const eventId = `$${Math.random()}`; const eventId = `$${Math.random()}`;
if (roomId === room.roomId) { if (roomId === room.roomId) {
room.addLiveEvents([ room.addLiveEvents(
new MatrixEvent({ [
event_id: eventId, new MatrixEvent({
type: eventType, event_id: eventId,
room_id: roomId, type: eventType,
sender: FAKE_USER_ID_2, room_id: roomId,
content, sender: FAKE_USER_ID_2,
state_key: stateKey, content,
}), state_key: stateKey,
]); }),
],
{ addToState: true },
);
} }
return { event_id: eventId }; return { event_id: eventId };
}, },

View File

@ -6136,7 +6136,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.partitionThreadedEvents(matrixEvents); room.partitionThreadedEvents(matrixEvents);
this.processAggregatedTimelineEvents(room, timelineEvents); this.processAggregatedTimelineEvents(room, timelineEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline()); room.addEventsToTimeline(timelineEvents, true, true, room.getLiveTimeline());
this.processThreadEvents(room, threadedEvents, true); this.processThreadEvents(room, threadedEvents, true);
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event)); unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
@ -6248,7 +6248,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events); const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start); timelineSet.addEventsToTimeline(timelineEvents, true, false, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it. // The target event is not in a thread but process the contextual events, so we can show any threads around it.
this.processThreadEvents(timelineSet.room, threadedEvents, true); this.processThreadEvents(timelineSet.room, threadedEvents, true);
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents); this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
@ -6342,10 +6342,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timeline.initialiseState(res.state.map(mapper)); timeline.initialiseState(res.state.map(mapper));
} }
timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch); timelineSet.addEventsToTimeline(events, true, false, timeline, resNewer.next_batch);
if (!resOlder.next_batch) { if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id); const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null); timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
} }
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward); timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward); timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
@ -6399,10 +6399,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timeline = timelineSet.getLiveTimeline(); const timeline = timelineSet.getLiveTimeline();
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper)); timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
timelineSet.addEventsToTimeline(events, true, timeline, null); timelineSet.addEventsToTimeline(events, true, false, timeline, null);
if (!resOlder.next_batch) { if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id); const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null); timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
} }
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward); timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(null, Direction.Forward); timeline.setPaginationToken(null, Direction.Forward);
@ -6665,7 +6665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// No need to partition events for threads here, everything lives // No need to partition events for threads here, everything lives
// in the notification timeline set // in the notification timeline set
const timelineSet = eventTimeline.getTimelineSet(); const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents); this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
// if we've hit the end of the timeline, we need to stop trying to // if we've hit the end of the timeline, we need to stop trying to
@ -6708,7 +6708,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper()); const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet(); const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, matrixEvents); this.processAggregatedTimelineEvents(room, matrixEvents);
this.processThreadRoots(room, matrixEvents, backwards); this.processThreadRoots(room, matrixEvents, backwards);
@ -6756,12 +6756,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const newToken = res.next_batch; const newToken = res.next_batch;
const timelineSet = eventTimeline.getTimelineSet(); const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null); timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, newToken ?? null);
if (!newToken && backwards) { if (!newToken && backwards) {
const originalEvent = const originalEvent =
thread.rootEvent ?? thread.rootEvent ??
mapper(await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id)); mapper(await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id));
timelineSet.addEventsToTimeline([originalEvent], true, eventTimeline, null); timelineSet.addEventsToTimeline([originalEvent], true, false, eventTimeline, null);
} }
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents); this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
@ -6800,7 +6800,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timelineSet = eventTimeline.getTimelineSet(); const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents); const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token); timelineSet.addEventsToTimeline(timelineEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, timelineEvents); this.processAggregatedTimelineEvents(room, timelineEvents);
this.processThreadRoots( this.processThreadRoots(
room, room,

View File

@ -284,7 +284,13 @@ export class RoomWidgetClient extends MatrixClient {
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]); const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>)); const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
await this.syncApi!.injectRoomEvents(this.room!, [], events); if (this.syncApi instanceof SyncApi) {
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
} else {
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
}
events.forEach((event) => { events.forEach((event) => {
this.emit(ClientEvent.Event, event); this.emit(ClientEvent.Event, event);
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
@ -567,7 +573,34 @@ export class RoomWidgetClient extends MatrixClient {
// Only inject once we have update the txId // Only inject once we have update the txId
await this.updateTxId(event); await this.updateTxId(event);
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
if (this.syncApi instanceof SyncApi) {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
// // -> state events part of the `timelineEventList` parameter will update the state.
// this.injectRoomEvents(this.room!, [], undefined, [event]);
// } else {
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
} else {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// this.injectRoomEvents(this.room!, [], [event]);
// } else {
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
}
this.emit(ClientEvent.Event, event); this.emit(ClientEvent.Event, event);
this.setSyncState(SyncState.Syncing); this.setSyncState(SyncState.Syncing);
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);

View File

@ -58,13 +58,13 @@ export interface IRoomTimelineData {
} }
export interface IAddEventToTimelineOptions export interface IAddEventToTimelineOptions
extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty"> { extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty" | "addToState"> {
/** Whether the sync response came from cache */ /** Whether the sync response came from cache */
fromCache?: boolean; fromCache?: boolean;
} }
export interface IAddLiveEventOptions export interface IAddLiveEventOptions
extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty"> { extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty" | "addToState"> {
/** Applies to events in the timeline only. If this is 'replace' then if a /** Applies to events in the timeline only. If this is 'replace' then if a
* duplicate is encountered, the event passed to this function will replace * duplicate is encountered, the event passed to this function will replace
* the existing event in the timeline. If this is not specified, or is * the existing event in the timeline. If this is not specified, or is
@ -391,6 +391,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
public addEventsToTimeline( public addEventsToTimeline(
events: MatrixEvent[], events: MatrixEvent[],
toStartOfTimeline: boolean, toStartOfTimeline: boolean,
addToState: boolean,
timeline: EventTimeline, timeline: EventTimeline,
paginationToken?: string | null, paginationToken?: string | null,
): void { ): void {
@ -495,6 +496,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// we don't know about this event yet. Just add it to the timeline. // we don't know about this event yet. Just add it to the timeline.
this.addEventToTimeline(event, timeline, { this.addEventToTimeline(event, timeline, {
toStartOfTimeline, toStartOfTimeline,
addToState,
}); });
lastEventWasNew = true; lastEventWasNew = true;
didUpdate = true; didUpdate = true;
@ -592,7 +594,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
*/ */
public addLiveEvent( public addLiveEvent(
event: MatrixEvent, event: MatrixEvent,
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions = {}, { duplicateStrategy, fromCache, roomState, timelineWasEmpty, addToState }: IAddLiveEventOptions,
): void { ): void {
if (this.filter) { if (this.filter) {
const events = this.filter.filterRoomTimeline([event]); const events = this.filter.filterRoomTimeline([event]);
@ -630,6 +632,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
fromCache, fromCache,
roomState, roomState,
timelineWasEmpty, timelineWasEmpty,
addToState,
}); });
} }
@ -649,40 +652,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
public addEventToTimeline( public addEventToTimeline(
event: MatrixEvent, event: MatrixEvent,
timeline: EventTimeline, timeline: EventTimeline,
{ toStartOfTimeline, fromCache, roomState, timelineWasEmpty }: IAddEventToTimelineOptions, { toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty, addToState }: IAddEventToTimelineOptions,
): void;
/**
* @deprecated In favor of the overload with `IAddEventToTimelineOptions`
*/
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
toStartOfTimeline: boolean,
fromCache?: boolean,
roomState?: RoomState,
): void;
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
toStartOfTimelineOrOpts: boolean | IAddEventToTimelineOptions,
fromCache = false,
roomState?: RoomState,
): void { ): void {
let toStartOfTimeline = !!toStartOfTimelineOrOpts;
let timelineWasEmpty: boolean | undefined;
if (typeof toStartOfTimelineOrOpts === "object") {
({ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
} else if (toStartOfTimelineOrOpts !== undefined) {
// Deprecation warning
// FIXME: Remove after 2023-06-01 (technical debt)
logger.warn(
"Overload deprecated: " +
"`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " +
"is deprecated in favor of the overload with " +
"`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`",
);
}
if (timeline.getTimelineSet() !== this) { if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " + throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`); "in timelineSet(threadId=${this.thread?.id})`);
@ -713,6 +684,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
toStartOfTimeline, toStartOfTimeline,
roomState, roomState,
timelineWasEmpty, timelineWasEmpty,
addToState,
}); });
this._eventIdToTimeline.set(eventId, timeline); this._eventIdToTimeline.set(eventId, timeline);
@ -741,7 +713,12 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @remarks * @remarks
* Fires {@link RoomEvent.Timeline} * Fires {@link RoomEvent.Timeline}
*/ */
public insertEventIntoTimeline(event: MatrixEvent, timeline: EventTimeline, roomState: RoomState): void { public insertEventIntoTimeline(
event: MatrixEvent,
timeline: EventTimeline,
roomState: RoomState,
addToState: boolean,
): void {
if (timeline.getTimelineSet() !== this) { if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.insertEventIntoTimeline: Timeline=${timeline.toString()} does not belong " + throw new Error(`EventTimelineSet.insertEventIntoTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`); "in timelineSet(threadId=${this.thread?.id})`);
@ -777,6 +754,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
fromCache: false, fromCache: false,
timelineWasEmpty: false, timelineWasEmpty: false,
roomState, roomState,
addToState,
}); });
return; return;
} }
@ -799,7 +777,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// If we got to the end of the loop, insertIndex points at the end of // If we got to the end of the loop, insertIndex points at the end of
// the list. // the list.
timeline.insertEvent(event, insertIndex, roomState); timeline.insertEvent(event, insertIndex, roomState, addToState);
this._eventIdToTimeline.set(eventId, timeline); this._eventIdToTimeline.set(eventId, timeline);
const data: IRoomTimelineData = { const data: IRoomTimelineData = {
@ -832,6 +810,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) { } else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this.liveTimeline, { this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false, toStartOfTimeline: false,
addToState: false,
}); });
} }
} }

View File

@ -35,6 +35,11 @@ export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWas
toStartOfTimeline: boolean; toStartOfTimeline: boolean;
/** The state events to reconcile metadata from */ /** The state events to reconcile metadata from */
roomState?: RoomState; roomState?: RoomState;
/** Whether to add timeline events to the state as was done in legacy sync v2.
* If true then timeline events will be added to the state.
* In sync v2 with org.matrix.msc4222.use_state_after and simplified sliding sync,
* all state arrives explicitly and timeline events should not be added. */
addToState: boolean;
} }
export enum Direction { export enum Direction {
@ -362,7 +367,7 @@ export class EventTimeline {
*/ */
public addEvent( public addEvent(
event: MatrixEvent, event: MatrixEvent,
{ toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions = { toStartOfTimeline: false }, { toStartOfTimeline, roomState, timelineWasEmpty, addToState }: IAddEventOptions,
): void { ): void {
if (!roomState) { if (!roomState) {
roomState = toStartOfTimeline ? this.startState : this.endState; roomState = toStartOfTimeline ? this.startState : this.endState;
@ -374,7 +379,7 @@ export class EventTimeline {
EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline); EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
// modify state but only on unfiltered timelineSets // modify state but only on unfiltered timelineSets
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
roomState?.setStateEvents([event], { timelineWasEmpty }); roomState?.setStateEvents([event], { timelineWasEmpty });
// it is possible that the act of setting the state event means we // it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try // can set more metadata (specifically sender/target props), so try
@ -417,14 +422,14 @@ export class EventTimeline {
* *
* @internal * @internal
*/ */
public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState): void { public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState, addToState: boolean): void {
const timelineSet = this.getTimelineSet(); const timelineSet = this.getTimelineSet();
if (timelineSet.room) { if (timelineSet.room) {
EventTimeline.setEventMetadata(event, roomState, false); EventTimeline.setEventMetadata(event, roomState, false);
// modify state but only on unfiltered timelineSets // modify state but only on unfiltered timelineSets
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
roomState.setStateEvents([event], {}); roomState.setStateEvents([event], {});
// it is possible that the act of setting the state event means we // it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try // can set more metadata (specifically sender/target props), so try

View File

@ -1250,7 +1250,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
const timeline = room.getLiveTimeline(); const timeline = room.getLiveTimeline();
// We use insertEventIntoTimeline to insert it in timestamp order, // We use insertEventIntoTimeline to insert it in timestamp order,
// because we don't know where it should go (until we have MSC4033). // because we don't know where it should go (until we have MSC4033).
timeline.getTimelineSet().insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!); timeline
.getTimelineSet()
.insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!, false);
} }
/** /**

View File

@ -1739,10 +1739,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
public addEventsToTimeline( public addEventsToTimeline(
events: MatrixEvent[], events: MatrixEvent[],
toStartOfTimeline: boolean, toStartOfTimeline: boolean,
addToState: boolean,
timeline: EventTimeline, timeline: EventTimeline,
paginationToken?: string, paginationToken?: string,
): void { ): void {
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken); timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, addToState, timeline, paginationToken);
} }
/** /**
@ -1907,7 +1908,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// see https://github.com/vector-im/vector-web/issues/2109 // see https://github.com/vector-im/vector-web/issues/2109
unfilteredLiveTimeline.getEvents().forEach(function (event) { unfilteredLiveTimeline.getEvents().forEach(function (event) {
timelineSet.addLiveEvent(event); timelineSet.addLiveEvent(event, { addToState: false }); // Filtered timeline sets should not track state
}); });
// find the earliest unfiltered timeline // find the earliest unfiltered timeline
@ -1994,6 +1995,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (filterType !== ThreadFilterType.My || currentUserParticipated) { if (filterType !== ThreadFilterType.My || currentUserParticipated) {
timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, { timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, {
toStartOfTimeline: false, toStartOfTimeline: false,
addToState: false,
}); });
} }
}); });
@ -2068,6 +2070,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const opts = { const opts = {
duplicateStrategy: DuplicateStrategy.Ignore, duplicateStrategy: DuplicateStrategy.Ignore,
fromCache: false, fromCache: false,
addToState: false,
roomState, roomState,
}; };
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts); this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
@ -2190,6 +2193,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy: DuplicateStrategy.Replace, duplicateStrategy: DuplicateStrategy.Replace,
fromCache: false, fromCache: false,
roomState, roomState,
addToState: false,
}); });
} }
} }
@ -2381,9 +2385,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy: DuplicateStrategy.Replace, duplicateStrategy: DuplicateStrategy.Replace,
fromCache: false, fromCache: false,
roomState: this.currentState, roomState: this.currentState,
addToState: false,
}); });
} else { } else {
timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline }); timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), {
toStartOfTimeline,
addToState: false,
});
} }
} }
}; };
@ -2540,7 +2548,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* Fires {@link RoomEvent.Timeline} * Fires {@link RoomEvent.Timeline}
*/ */
private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void { private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void {
const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions; const { duplicateStrategy, timelineWasEmpty, fromCache, addToState } = addLiveEventOptions;
// add to our timeline sets // add to our timeline sets
for (const timelineSet of this.timelineSets) { for (const timelineSet of this.timelineSets) {
@ -2548,6 +2556,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy, duplicateStrategy,
fromCache, fromCache,
timelineWasEmpty, timelineWasEmpty,
addToState,
}); });
} }
@ -2631,11 +2640,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) { if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
toStartOfTimeline: false, toStartOfTimeline: false,
addToState: false, // We don't support localEcho of state events yet
}); });
} }
} else { } else {
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
toStartOfTimeline: false, toStartOfTimeline: false,
addToState: false, // We don't support localEcho of state events yet
}); });
} }
} }
@ -2886,8 +2897,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @param addLiveEventOptions - addLiveEvent options * @param addLiveEventOptions - addLiveEvent options
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'. * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
*/ */
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> { public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions: IAddLiveEventOptions): Promise<void> {
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {}; const { duplicateStrategy, fromCache, timelineWasEmpty = false, addToState } = addLiveEventOptions;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
} }
@ -2902,6 +2913,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy, duplicateStrategy,
fromCache, fromCache,
timelineWasEmpty, timelineWasEmpty,
addToState,
}; };
// List of extra events to check for being parents of any relations encountered // List of extra events to check for being parents of any relations encountered

View File

@ -208,6 +208,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
public static setServerSideSupport(status: FeatureSupport): void { public static setServerSideSupport(status: FeatureSupport): void {
Thread.hasServerSideSupport = status; Thread.hasServerSideSupport = status;
// XXX: This global latching behaviour is really unexpected and means that you can't undo when moving to a server without support
if (status !== FeatureSupport.Stable) { if (status !== FeatureSupport.Stable) {
FILTER_RELATED_BY_SENDERS.setPreferUnstable(true); FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true); FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
@ -317,6 +318,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
toStartOfTimeline, toStartOfTimeline,
fromCache: false, fromCache: false,
roomState: this.roomState, roomState: this.roomState,
addToState: false,
}); });
} }
} }
@ -343,7 +345,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
if (this.findEventById(eventId)) { if (this.findEventById(eventId)) {
return; return;
} }
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState); this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState, false);
} }
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
@ -618,7 +620,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
// if the thread has regular events, this will just load the last reply. // 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. // if the thread is newly created, this will load the root event.
if (this.replyCount === 0 && this.rootEvent) { if (this.replyCount === 0 && this.rootEvent) {
this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null); this.timelineSet.addEventsToTimeline([this.rootEvent], true, false, this.liveTimeline, null);
this.liveTimeline.setPaginationToken(null, Direction.Backward); this.liveTimeline.setPaginationToken(null, Direction.Backward);
} else { } else {
this.initalEventFetchProm = this.client.paginateEventTimeline(this.liveTimeline, { this.initalEventFetchProm = this.client.paginateEventTimeline(this.liveTimeline, {

View File

@ -612,7 +612,7 @@ export class SlidingSyncSdk {
timelineEvents = newEvents; timelineEvents = newEvents;
if (oldEvents.length > 0) { if (oldEvents.length > 0) {
// old events are scrollback, insert them now // old events are scrollback, insert them now
room.addEventsToTimeline(oldEvents, true, room.getLiveTimeline(), roomData.prev_batch); room.addEventsToTimeline(oldEvents, true, false, room.getLiveTimeline(), roomData.prev_batch);
} }
} }
@ -754,7 +754,7 @@ export class SlidingSyncSdk {
/** /**
* Injects events into a room's model. * Injects events into a room's model.
* @param stateEventList - A list of state events. This is the state * @param stateEventList - A list of state events. This is the state
* at the *START* of the timeline list if it is supplied. * at the *END* of the timeline list if it is supplied.
* @param timelineEventList - A list of timeline events. Lower index * @param timelineEventList - A list of timeline events. Lower index
* is earlier in time. Higher index is later. * is earlier in time. Higher index is later.
* @param numLive - the number of events in timelineEventList which just happened, * @param numLive - the number of events in timelineEventList which just happened,
@ -763,13 +763,9 @@ export class SlidingSyncSdk {
public async injectRoomEvents( public async injectRoomEvents(
room: Room, room: Room,
stateEventList: MatrixEvent[], stateEventList: MatrixEvent[],
timelineEventList?: MatrixEvent[], timelineEventList: MatrixEvent[] = [],
numLive?: number, numLive: number = 0,
): Promise<void> { ): Promise<void> {
timelineEventList = timelineEventList || [];
stateEventList = stateEventList || [];
numLive = numLive || 0;
// If there are no events in the timeline yet, initialise it with // If there are no events in the timeline yet, initialise it with
// the given state events // the given state events
const liveTimeline = room.getLiveTimeline(); const liveTimeline = room.getLiveTimeline();
@ -820,16 +816,17 @@ export class SlidingSyncSdk {
timelineEventList = timelineEventList.slice(0, -1 * liveTimelineEvents.length); timelineEventList = timelineEventList.slice(0, -1 * liveTimelineEvents.length);
} }
// execute the timeline events. This will continue to diverge the current state // Execute the timeline events.
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need // This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc. // to be decorated with sender etc.
await room.addLiveEvents(timelineEventList, { await room.addLiveEvents(timelineEventList, {
fromCache: true, fromCache: true,
addToState: false,
}); });
if (liveTimelineEvents.length > 0) { if (liveTimelineEvents.length > 0) {
await room.addLiveEvents(liveTimelineEvents, { await room.addLiveEvents(liveTimelineEvents, {
fromCache: false, fromCache: false,
addToState: false,
}); });
} }
@ -966,7 +963,7 @@ export class SlidingSyncSdk {
return a.getTs() - b.getTs(); return a.getTs() - b.getTs();
}); });
this.notifEvents.forEach((event) => { this.notifEvents.forEach((event) => {
this.client.getNotifTimelineSet()?.addLiveEvent(event); this.client.getNotifTimelineSet()?.addLiveEvent(event, { addToState: false });
}); });
this.notifEvents = []; this.notifEvents = [];
} }

View File

@ -77,7 +77,9 @@ export interface ITimeline {
export interface IJoinedRoom { export interface IJoinedRoom {
"summary": IRoomSummary; "summary": IRoomSummary;
"state": IState; // One of `state` or `state_after` is required.
"state"?: IState;
"org.matrix.msc4222.state_after"?: IState; // https://github.com/matrix-org/matrix-spec-proposals/pull/4222
"timeline": ITimeline; "timeline": ITimeline;
"ephemeral": IEphemeral; "ephemeral": IEphemeral;
"account_data": IAccountData; "account_data": IAccountData;
@ -106,9 +108,11 @@ export interface IInvitedRoom {
} }
export interface ILeftRoom { export interface ILeftRoom {
state: IState; // One of `state` or `state_after` is required.
timeline: ITimeline; "state"?: IState;
account_data: IAccountData; "org.matrix.msc4222.state_after"?: IState;
"timeline": ITimeline;
"account_data": IAccountData;
} }
export interface IKnockedRoom { export interface IKnockedRoom {
@ -481,13 +485,18 @@ export class SyncAccumulator {
// Work out the current state. The deltas need to be applied in the order: // Work out the current state. The deltas need to be applied in the order:
// - existing state which didn't come down /sync. // - existing state which didn't come down /sync.
// - State events under the 'state' key. // - State events under the 'state' key.
// - State events in the 'timeline'. // - State events under the 'state_after' key OR state events in the 'timeline' if 'state_after' is not present.
data.state?.events?.forEach((e) => { data.state?.events?.forEach((e) => {
setState(currentData._currentState, e); setState(currentData._currentState, e);
}); });
data.timeline?.events?.forEach((e, index) => { data["org.matrix.msc4222.state_after"]?.events?.forEach((e) => {
// this nops if 'e' isn't a state event
setState(currentData._currentState, e); setState(currentData._currentState, e);
});
data.timeline?.events?.forEach((e, index) => {
if (!data["org.matrix.msc4222.state_after"]) {
// this nops if 'e' isn't a state event
setState(currentData._currentState, e);
}
// append the event to the timeline. The back-pagination token // append the event to the timeline. The back-pagination token
// corresponds to the first event in the timeline // corresponds to the first event in the timeline
let transformedEvent: TaggedEvent; let transformedEvent: TaggedEvent;
@ -563,17 +572,22 @@ export class SyncAccumulator {
}); });
Object.keys(this.joinRooms).forEach((roomId) => { Object.keys(this.joinRooms).forEach((roomId) => {
const roomData = this.joinRooms[roomId]; const roomData = this.joinRooms[roomId];
const roomJson: IJoinedRoom = { const roomJson: IJoinedRoom & {
ephemeral: { events: [] }, // We track both `state` and `state_after` for downgrade compatibility
account_data: { events: [] }, "state": IState;
state: { events: [] }, "org.matrix.msc4222.state_after": IState;
timeline: { } = {
"ephemeral": { events: [] },
"account_data": { events: [] },
"state": { events: [] },
"org.matrix.msc4222.state_after": { events: [] },
"timeline": {
events: [], events: [],
prev_batch: null, prev_batch: null,
}, },
unread_notifications: roomData._unreadNotifications, "unread_notifications": roomData._unreadNotifications,
unread_thread_notifications: roomData._unreadThreadNotifications, "unread_thread_notifications": roomData._unreadThreadNotifications,
summary: roomData._summary as IRoomSummary, "summary": roomData._summary as IRoomSummary,
}; };
// Add account data // Add account data
Object.keys(roomData._accountData).forEach((evType) => { Object.keys(roomData._accountData).forEach((evType) => {
@ -650,8 +664,11 @@ export class SyncAccumulator {
Object.keys(roomData._currentState).forEach((evType) => { Object.keys(roomData._currentState).forEach((evType) => {
Object.keys(roomData._currentState[evType]).forEach((stateKey) => { Object.keys(roomData._currentState[evType]).forEach((stateKey) => {
let ev = roomData._currentState[evType][stateKey]; let ev = roomData._currentState[evType][stateKey];
// Push to both fields to provide downgrade compatibility in the sync accumulator db
// the code will prefer `state_after` if it is present
roomJson["org.matrix.msc4222.state_after"].events.push(ev);
// Roll the state back to the value at the start of the timeline if it was changed
if (rollBackState[evType] && rollBackState[evType][stateKey]) { if (rollBackState[evType] && rollBackState[evType][stateKey]) {
// use the reverse clobbered event instead.
ev = rollBackState[evType][stateKey]; ev = rollBackState[evType][stateKey];
} }
roomJson.state.events.push(ev); roomJson.state.events.push(ev);

View File

@ -175,14 +175,15 @@ export enum SetPresence {
} }
interface ISyncParams { interface ISyncParams {
filter?: string; "filter"?: string;
timeout: number; "timeout": number;
since?: string; "since"?: string;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
full_state?: boolean; "full_state"?: boolean;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
set_presence?: SetPresence; "set_presence"?: SetPresence;
_cacheBuster?: string | number; // not part of the API itself "_cacheBuster"?: string | number; // not part of the API itself
"org.matrix.msc4222.use_state_after"?: boolean; // https://github.com/matrix-org/matrix-spec-proposals/pull/4222
} }
type WrappedRoom<T> = T & { type WrappedRoom<T> = T & {
@ -344,8 +345,9 @@ export class SyncApi {
); );
const qps: ISyncParams = { const qps: ISyncParams = {
timeout: 0, // don't want to block since this is a single isolated req "timeout": 0, // don't want to block since this is a single isolated req
filter: filterId, "filter": filterId,
"org.matrix.msc4222.use_state_after": true,
}; };
const data = await client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, { const data = await client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
@ -375,21 +377,18 @@ export class SyncApi {
prev_batch: null, prev_batch: null,
events: [], events: [],
}; };
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any // set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating. // events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS); room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
await this.injectRoomEvents(room, stateEvents, events); const { timelineEvents } = await this.mapAndInjectRoomEvents(leaveObj);
room.recalculate(); room.recalculate();
client.store.storeRoom(room); client.store.storeRoom(room);
client.emit(ClientEvent.Room, room); client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events); this.processEventsForNotifs(room, timelineEvents);
return room; return room;
}), }),
); );
@ -464,6 +463,7 @@ export class SyncApi {
this._peekRoom.addEventsToTimeline( this._peekRoom.addEventsToTimeline(
messages.reverse(), messages.reverse(),
true, true,
true,
this._peekRoom.getLiveTimeline(), this._peekRoom.getLiveTimeline(),
response.messages.start, response.messages.start,
); );
@ -551,7 +551,7 @@ export class SyncApi {
}) })
.map(this.client.getEventMapper()); .map(this.client.getEventMapper());
await peekRoom.addLiveEvents(events); await peekRoom.addLiveEvents(events, { addToState: true });
this.peekPoll(peekRoom, res.end); this.peekPoll(peekRoom, res.end);
}, },
(err) => { (err) => {
@ -976,7 +976,11 @@ export class SyncApi {
filter = this.getGuestFilter(); filter = this.getGuestFilter();
} }
const qps: ISyncParams = { filter, timeout }; const qps: ISyncParams = {
filter,
timeout,
"org.matrix.msc4222.use_state_after": true,
};
if (this.opts.disablePresence) { if (this.opts.disablePresence) {
qps.set_presence = SetPresence.Offline; qps.set_presence = SetPresence.Offline;
@ -1242,7 +1246,7 @@ export class SyncApi {
const room = inviteObj.room; const room = inviteObj.room;
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room); const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
await this.injectRoomEvents(room, stateEvents); await this.injectRoomEvents(room, stateEvents, undefined);
const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender(); const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender();
@ -1282,15 +1286,24 @@ export class SyncApi {
await promiseMapSeries(joinRooms, async (joinObj) => { await promiseMapSeries(joinRooms, async (joinObj) => {
const room = joinObj.room; const room = joinObj.room;
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room); const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
// Prevent events from being decrypted ahead of time // Prevent events from being decrypted ahead of time
// this helps large account to speed up faster // this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events // room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly // required for a client to function properly
const events = this.mapSyncEventsFormat(joinObj.timeline, room, false); const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral); const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data); const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
const encrypted = this.isRoomEncrypted(room, stateEvents, events); // If state_after is present, this is the events that form the state at the end of the timeline block and
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
// is absent so we explicitly check the field on the original object.
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
? stateAfterEvents
: stateEvents.concat(timelineEvents);
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
// We store the server-provided value first so it's correct when any of the events fire. // We store the server-provided value first so it's correct when any of the events fire.
if (joinObj.unread_notifications) { if (joinObj.unread_notifications) {
/** /**
@ -1378,8 +1391,8 @@ export class SyncApi {
// which we'll try to paginate but not get any new events (which // which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain). // will stop us linking the empty timeline into the chain).
// //
for (let i = events.length - 1; i >= 0; i--) { for (let i = timelineEvents.length - 1; i >= 0; i--) {
const eventId = events[i].getId()!; const eventId = timelineEvents[i].getId()!;
if (room.getTimelineForEvent(eventId)) { if (room.getTimelineForEvent(eventId)) {
debuglog(`Already have event ${eventId} in limited sync - not resetting`); debuglog(`Already have event ${eventId} in limited sync - not resetting`);
limited = false; limited = false;
@ -1387,7 +1400,7 @@ export class SyncApi {
// we might still be missing some of the events before i; // we might still be missing some of the events before i;
// we don't want to be adding them to the end of the // we don't want to be adding them to the end of the
// timeline because that would put them out of order. // timeline because that would put them out of order.
events.splice(0, i); timelineEvents.splice(0, i);
// XXX: there's a problem here if the skipped part of the // XXX: there's a problem here if the skipped part of the
// timeline modifies the state set in stateEvents, because // timeline modifies the state set in stateEvents, because
@ -1419,8 +1432,9 @@ export class SyncApi {
// avoids a race condition if the application tries to send a message after the // avoids a race condition if the application tries to send a message after the
// state event is processed, but before crypto is enabled, which then causes the // state event is processed, but before crypto is enabled, which then causes the
// crypto layer to complain. // crypto layer to complain.
if (this.syncOpts.cryptoCallbacks) { if (this.syncOpts.cryptoCallbacks) {
for (const e of stateEvents.concat(events)) { for (const e of eventsFormingFinalState) {
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") { if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e); await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
} }
@ -1428,7 +1442,17 @@ export class SyncApi {
} }
try { try {
await this.injectRoomEvents(room, stateEvents, events, syncEventData.fromCache); if ("org.matrix.msc4222.state_after" in joinObj) {
await this.injectRoomEvents(
room,
undefined,
stateAfterEvents,
timelineEvents,
syncEventData.fromCache,
);
} else {
await this.injectRoomEvents(room, stateEvents, undefined, timelineEvents, syncEventData.fromCache);
}
} catch (e) { } catch (e) {
logger.error(`Failed to process events on room ${room.roomId}:`, e); logger.error(`Failed to process events on room ${room.roomId}:`, e);
} }
@ -1452,11 +1476,11 @@ export class SyncApi {
client.emit(ClientEvent.Room, room); client.emit(ClientEvent.Room, room);
} }
this.processEventsForNotifs(room, events); this.processEventsForNotifs(room, timelineEvents);
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e); const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
stateEvents.forEach(emitEvent); stateEvents.forEach(emitEvent);
events.forEach(emitEvent); timelineEvents.forEach(emitEvent);
ephemeralEvents.forEach(emitEvent); ephemeralEvents.forEach(emitEvent);
accountDataEvents.forEach(emitEvent); accountDataEvents.forEach(emitEvent);
@ -1469,11 +1493,9 @@ export class SyncApi {
// Handle leaves (e.g. kicked rooms) // Handle leaves (e.g. kicked rooms)
await promiseMapSeries(leaveRooms, async (leaveObj) => { await promiseMapSeries(leaveRooms, async (leaveObj) => {
const room = leaveObj.room; const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data); const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
await this.injectRoomEvents(room, stateEvents, events);
room.addAccountData(accountDataEvents); room.addAccountData(accountDataEvents);
room.recalculate(); room.recalculate();
@ -1482,12 +1504,15 @@ export class SyncApi {
client.emit(ClientEvent.Room, room); client.emit(ClientEvent.Room, room);
} }
this.processEventsForNotifs(room, events); this.processEventsForNotifs(room, timelineEvents);
stateEvents.forEach(function (e) { stateEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e); client.emit(ClientEvent.Event, e);
}); });
events.forEach(function (e) { stateAfterEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
timelineEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e); client.emit(ClientEvent.Event, e);
}); });
accountDataEvents.forEach(function (e) { accountDataEvents.forEach(function (e) {
@ -1500,7 +1525,7 @@ export class SyncApi {
const room = knockObj.room; const room = knockObj.room;
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room); const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
await this.injectRoomEvents(room, stateEvents); await this.injectRoomEvents(room, stateEvents, undefined);
if (knockObj.isBrandNewRoom) { if (knockObj.isBrandNewRoom) {
room.recalculate(); room.recalculate();
@ -1525,7 +1550,7 @@ export class SyncApi {
return a.getTs() - b.getTs(); return a.getTs() - b.getTs();
}); });
this.notifEvents.forEach(function (event) { this.notifEvents.forEach(function (event) {
client.getNotifTimelineSet()?.addLiveEvent(event); client.getNotifTimelineSet()?.addLiveEvent(event, { addToState: true });
}); });
} }
@ -1669,7 +1694,7 @@ export class SyncApi {
} }
private mapSyncEventsFormat( private mapSyncEventsFormat(
obj: IInviteState | ITimeline | IEphemeral, obj: IInviteState | ITimeline | IEphemeral | undefined,
room?: Room, room?: Room,
decrypt = true, decrypt = true,
): MatrixEvent[] { ): MatrixEvent[] {
@ -1737,28 +1762,69 @@ export class SyncApi {
// When processing the sync response we cannot rely on Room.hasEncryptionStateEvent we actually // When processing the sync response we cannot rely on Room.hasEncryptionStateEvent we actually
// inject the events into the room object, so we have to inspect the events themselves. // inject the events into the room object, so we have to inspect the events themselves.
private isRoomEncrypted(room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[]): boolean { private isRoomEncrypted(room: Room, eventsFormingFinalState: MatrixEvent[]): boolean {
return ( return room.hasEncryptionStateEvent() || !!this.findEncryptionEvent(eventsFormingFinalState);
room.hasEncryptionStateEvent() || }
!!this.findEncryptionEvent(stateEventList) ||
!!this.findEncryptionEvent(timelineEventList) private async mapAndInjectRoomEvents(wrappedRoom: WrappedRoom<ILeftRoom>): Promise<{
timelineEvents: MatrixEvent[];
stateEvents?: MatrixEvent[];
stateAfterEvents?: MatrixEvent[];
}> {
const stateEvents = this.mapSyncEventsFormat(wrappedRoom.state, wrappedRoom.room);
const stateAfterEvents = this.mapSyncEventsFormat(
wrappedRoom["org.matrix.msc4222.state_after"],
wrappedRoom.room,
); );
const timelineEvents = this.mapSyncEventsFormat(wrappedRoom.timeline, wrappedRoom.room);
if ("org.matrix.msc4222.state_after" in wrappedRoom) {
await this.injectRoomEvents(wrappedRoom.room, undefined, stateAfterEvents, timelineEvents);
} else {
await this.injectRoomEvents(wrappedRoom.room, stateEvents, undefined, timelineEvents);
}
return { timelineEvents, stateEvents, stateAfterEvents };
} }
/** /**
* Injects events into a room's model. * Injects events into a room's model.
* @param stateEventList - A list of state events. This is the state * @param stateEventList - A list of state events. This is the state
* at the *START* of the timeline list if it is supplied. * at the *START* of the timeline list if it is supplied.
* @param stateAfterEventList - A list of state events. This is the state
* at the *END* of the timeline list if it is supplied.
* @param timelineEventList - A list of timeline events, including threaded. Lower index * @param timelineEventList - A list of timeline events, including threaded. Lower index
* is earlier in time. Higher index is later. * is earlier in time. Higher index is later.
* @param fromCache - whether the sync response came from cache * @param fromCache - whether the sync response came from cache
*
* No more than one of stateEventList and stateAfterEventList must be supplied. If
* stateEventList is supplied, the events in timelineEventList are added to the state
* after stateEventList. If stateAfterEventList is supplied, the events in timelineEventList
* are not added to the state.
*/ */
public async injectRoomEvents( public async injectRoomEvents(
room: Room, room: Room,
stateEventList: MatrixEvent[], stateEventList: MatrixEvent[],
stateAfterEventList: undefined,
timelineEventList?: MatrixEvent[],
fromCache?: boolean,
): Promise<void>;
public async injectRoomEvents(
room: Room,
stateEventList: undefined,
stateAfterEventList: MatrixEvent[],
timelineEventList?: MatrixEvent[],
fromCache?: boolean,
): Promise<void>;
public async injectRoomEvents(
room: Room,
stateEventList: MatrixEvent[] | undefined,
stateAfterEventList: MatrixEvent[] | undefined,
timelineEventList?: MatrixEvent[], timelineEventList?: MatrixEvent[],
fromCache = false, fromCache = false,
): Promise<void> { ): Promise<void> {
const eitherStateEventList = stateAfterEventList ?? stateEventList!;
// If there are no events in the timeline yet, initialise it with // If there are no events in the timeline yet, initialise it with
// the given state events // the given state events
const liveTimeline = room.getLiveTimeline(); const liveTimeline = room.getLiveTimeline();
@ -1772,10 +1838,11 @@ export class SyncApi {
// push actions cache elsewhere so we can freeze MatrixEvents, or otherwise // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise
// find some solution where MatrixEvents are immutable but allow for a cache // find some solution where MatrixEvents are immutable but allow for a cache
// field. // field.
for (const ev of stateEventList) {
for (const ev of eitherStateEventList) {
this.client.getPushActionsForEvent(ev); this.client.getPushActionsForEvent(ev);
} }
liveTimeline.initialiseState(stateEventList, { liveTimeline.initialiseState(eitherStateEventList, {
timelineWasEmpty, timelineWasEmpty,
}); });
} }
@ -1807,17 +1874,18 @@ export class SyncApi {
// XXX: As above, don't do this... // XXX: As above, don't do this...
//room.addLiveEvents(stateEventList || []); //room.addLiveEvents(stateEventList || []);
// Do this instead... // Do this instead...
room.oldState.setStateEvents(stateEventList || []); room.oldState.setStateEvents(eitherStateEventList);
room.currentState.setStateEvents(stateEventList || []); room.currentState.setStateEvents(eitherStateEventList);
} }
// Execute the timeline events. This will continue to diverge the current state // Execute the timeline events. If addToState is true the timeline has any state
// if the timeline has any state events in it. // events in it, this will continue to diverge the current state.
// This also needs to be done before running push rules on the events as they need // This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc. // to be decorated with sender etc.
await room.addLiveEvents(timelineEventList || [], { await room.addLiveEvents(timelineEventList || [], {
fromCache, fromCache,
timelineWasEmpty, timelineWasEmpty,
addToState: stateAfterEventList === undefined,
}); });
this.client.processBeaconEvents(room, timelineEventList); this.client.processBeaconEvents(room, timelineEventList);
} }