1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Ensure we do not add relations to the wrong timeline (#3427)

* Do not assume that a relation lives in main timeline if we do not know its parent

* For pagination, partition relations with unknown parents into a separate bucket

And only add them to relation map, no timelines

* Make addLiveEvents async and have it fetch parent events of unknown relations to not insert into the wrong timeline

* Fix tests not awaiting addLIveEvents

* Fix handling of thread roots in eventShouldLiveIn

* Fix types

* Fix tests

* Fix import

* Stash thread ID of relations in unsigned to be stashed in sync accumulator

* Persist after processing

* Revert "Persist after processing"

This reverts commit 05ed6409b3.

* Update unsigned field name to match MSC4023

* Persist after processing to store thread id in unsigned sync accumulator

* Add test

* Fix replayEvents getting doubled up due to Thread::addEvents being called in createThread and separately

* Fix test

* Switch to using UnstableValue

* Add comment

* Iterate
This commit is contained in:
Michael Telatynski
2023-06-01 16:29:05 +01:00
committed by GitHub
parent 9c5c7ddb17
commit 71f9b25db7
12 changed files with 517 additions and 322 deletions

View File

@@ -1142,7 +1142,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
room.addLiveEvents([THREAD_REPLY2]); await room.addLiveEvents([THREAD_REPLY2]);
httpBackend httpBackend
.when( .when(
"GET", "GET",
@@ -1156,7 +1156,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
room.addLiveEvents([THREAD_REPLY3]); await room.addLiveEvents([THREAD_REPLY3]);
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([
@@ -1248,7 +1248,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
room.addLiveEvents([THREAD_REPLY2]); await room.addLiveEvents([THREAD_REPLY2]);
httpBackend httpBackend
.when( .when(
"GET", "GET",
@@ -1267,7 +1267,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
room.addLiveEvents([THREAD_REPLY3]); await room.addLiveEvents([THREAD_REPLY3]);
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([
@@ -1572,7 +1572,7 @@ describe("MatrixClient event timelines", function () {
respondToEvent(THREAD_ROOT_UPDATED); respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD_ROOT_UPDATED); respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD2_ROOT); respondToEvent(THREAD2_ROOT);
room.addLiveEvents([THREAD_REPLY2]); await room.addLiveEvents([THREAD_REPLY2]);
await httpBackend.flushAllExpected(); await httpBackend.flushAllExpected();
await prom; await prom;
expect(thread.length).toBe(2); expect(thread.length).toBe(2);
@@ -1937,11 +1937,6 @@ describe("MatrixClient event timelines", function () {
.respond(200, function () { .respond(200, function () {
return THREAD_ROOT; return THREAD_ROOT;
}); });
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend httpBackend
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!)) .when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () { .respond(200, function () {

View File

@@ -36,11 +36,15 @@ import {
NotificationCountType, NotificationCountType,
IEphemeral, IEphemeral,
Room, Room,
IndexedDBStore,
RelationType,
} from "../../src"; } from "../../src";
import { ReceiptType } from "../../src/@types/read_receipts"; import { ReceiptType } from "../../src/@types/read_receipts";
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync"; import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
describe("MatrixClient syncing", () => { describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost"; const selfUserId = "@alice:localhost";
@@ -1867,4 +1871,124 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbClient.stopClient(); idbClient.stopClient();
idbHttpBackend.stop(); idbHttpBackend.stop();
}); });
it("should query server for which thread a 2nd order relation belongs to and stash in sync accumulator", async () => {
const roomId = "!room:example.org";
async function startClient(client: MatrixClient): Promise<void> {
await Promise.all([
idbClient.startClient({
// Without this all events just go into the main timeline
threadSupport: true,
}),
idbHttpBackend.flushAllExpected(),
emitPromise(idbClient, ClientEvent.Room),
]);
}
function assertEventsExpected(client: MatrixClient): void {
const room = client.getRoom(roomId);
const mainTimelineEvents = room!.getLiveTimeline().getEvents();
expect(mainTimelineEvents).toHaveLength(1);
expect(mainTimelineEvents[0].getContent().body).toEqual("Test");
const thread = room!.getThread("$someThreadId")!;
expect(thread.replayEvents).toHaveLength(1);
expect(thread.replayEvents![0].getRelation()!.key).toEqual("🪿");
}
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
dbName: "test",
}),
});
let idbHttpBackend = idbTestClient.httpBackend;
let idbClient = idbTestClient.client;
await idbClient.store.startup();
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
const syncRoomSection = {
join: {
[roomId]: {
timeline: {
prev_batch: "foo",
events: [
mkMessage({
room: roomId,
user: selfUserId,
msg: "Test",
}),
mkEvent({
room: roomId,
user: selfUserId,
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: "$someUnknownEvent",
key: "🪿",
},
},
type: "m.reaction",
}),
],
},
},
},
};
idbHttpBackend.when("GET", "/sync").respond(200, {
...syncData,
rooms: syncRoomSection,
});
idbHttpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/event/%24someUnknownEvent`).respond(
200,
mkEvent({
room: roomId,
user: selfUserId,
content: {
"body": "Thread response",
"m.relates_to": {
rel_type: THREAD_RELATION_TYPE.name,
event_id: "$someThreadId",
},
},
type: "m.room.message",
}),
);
await startClient(idbClient);
assertEventsExpected(idbClient);
idbHttpBackend.verifyNoOutstandingExpectation();
// Force sync accumulator to persist, reset client, assert it doesn't re-fetch event on next start-up
await idbClient.store.save(true);
await idbClient.stopClient();
await idbClient.store.destroy();
await idbHttpBackend.stop();
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
dbName: "test",
}),
});
idbHttpBackend = idbTestClient.httpBackend;
idbClient = idbTestClient.client;
await idbClient.store.startup();
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
idbHttpBackend.when("GET", "/sync").respond(200, syncData);
await startClient(idbClient);
assertEventsExpected(idbClient);
idbHttpBackend.verifyNoOutstandingExpectation();
await idbClient.stopClient();
await idbHttpBackend.stop();
});
}); });

View File

@@ -89,7 +89,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)!;
room.addLiveEvents([thread.rootEvent]); await room.addLiveEvents([thread.rootEvent]);
// 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

@@ -42,6 +42,7 @@ import { SyncApiOptions, SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client"; import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger"; import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils"; import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
describe("SlidingSyncSdk", () => { describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined; let client: MatrixClient | undefined;
@@ -301,67 +302,57 @@ describe("SlidingSyncSdk", () => {
}, },
}; };
it("can be created with required_state and timeline", () => { it("can be created with required_state and timeline", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.name).toEqual(data[roomA].name);
return; expect(gotRoom!.getMyMembership()).toEqual("join");
} assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
}); });
it("can be created with timeline only", () => { it("can be created with timeline only", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomB); const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.name).toEqual(data[roomB].name);
return; expect(gotRoom!.getMyMembership()).toEqual("join");
} assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
}); });
it("can be created with a highlight_count", () => { it("can be created with a highlight_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomC); const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
data[roomC].highlight_count, data[roomC].highlight_count,
); );
}); });
it("can be created with a notification_count", () => { it("can be created with a notification_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomD); const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
data[roomD].notification_count, data[roomD].notification_count,
); );
}); });
it("can be created with an invited/joined_count", () => { it("can be created with an invited/joined_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomG); const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
return; expect(gotRoom!.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
}
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
}); });
it("can be created with live events", () => { it("can be created with live events", async () => {
let seenLiveEvent = false; const seenLiveEventDeferred = defer<boolean>();
const listener = ( const listener = (
ev: MatrixEvent, ev: MatrixEvent,
room?: Room, room?: Room,
@@ -371,43 +362,37 @@ describe("SlidingSyncSdk", () => {
) => { ) => {
if (timelineData?.liveEvent) { if (timelineData?.liveEvent) {
assertTimelineEvents([ev], data[roomH].timeline.slice(-1)); assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
seenLiveEvent = true; seenLiveEventDeferred.resolve(true);
} }
}; };
client!.on(RoomEvent.Timeline, listener); client!.on(RoomEvent.Timeline, listener);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
await emitPromise(client!, ClientEvent.Room);
client!.off(RoomEvent.Timeline, listener); client!.off(RoomEvent.Timeline, listener);
const gotRoom = client!.getRoom(roomH); const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.name).toEqual(data[roomH].name);
return; expect(gotRoom!.getMyMembership()).toEqual("join");
}
expect(gotRoom.name).toEqual(data[roomH].name);
expect(gotRoom.getMyMembership()).toEqual("join");
// check the entire timeline is correct // check the entire timeline is correct
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline); assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
expect(seenLiveEvent).toBe(true); await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
}); });
it("can be created with invite_state", () => { it("can be created with invite_state", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomE); const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.getMyMembership()).toEqual("invite");
return; expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
}
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
}); });
it("uses the 'name' field to caluclate the room name", () => { it("uses the 'name' field to caluclate the room name", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomF); const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { expect(gotRoom!.name).toEqual(data[roomF].name);
return;
}
expect(gotRoom.name).toEqual(data[roomF].name);
}); });
describe("updating", () => { describe("updating", () => {
@@ -419,33 +404,33 @@ describe("SlidingSyncSdk", () => {
name: data[roomA].name, name: data[roomA].name,
}); });
const gotRoom = client!.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { if (gotRoom == null) {
return; return;
} }
const newTimeline = data[roomA].timeline; const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent); newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline); assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-3), newTimeline);
}); });
it("can update with a new required_state event", async () => { it("can update with a new required_state event", async () => {
let gotRoom = client!.getRoom(roomB); let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { if (gotRoom == null) {
return; return;
} }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")], required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
timeline: [], timeline: [],
name: data[roomB].name, name: data[roomB].name,
}); });
gotRoom = client!.getRoom(roomB); gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { if (gotRoom == null) {
return; return;
} }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted); expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted);
}); });
it("can update with a new highlight_count", async () => { it("can update with a new highlight_count", async () => {
@@ -456,11 +441,11 @@ describe("SlidingSyncSdk", () => {
highlight_count: 1, highlight_count: 1,
}); });
const gotRoom = client!.getRoom(roomC); const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { if (gotRoom == null) {
return; return;
} }
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1); expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
}); });
it("can update with a new notification_count", async () => { it("can update with a new notification_count", async () => {
@@ -471,11 +456,11 @@ describe("SlidingSyncSdk", () => {
notification_count: 1, notification_count: 1,
}); });
const gotRoom = client!.getRoom(roomD); const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { if (gotRoom == null) {
return; return;
} }
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1); expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
}); });
it("can update with a new joined_count", () => { it("can update with a new joined_count", () => {
@@ -486,11 +471,11 @@ describe("SlidingSyncSdk", () => {
joined_count: 1, joined_count: 1,
}); });
const gotRoom = client!.getRoom(roomG); const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { if (gotRoom == null) {
return; return;
} }
expect(gotRoom.getJoinedMemberCount()).toEqual(1); expect(gotRoom!.getJoinedMemberCount()).toEqual(1);
}); });
// Regression test for a bug which caused the timeline entries to be out-of-order // Regression test for a bug which caused the timeline entries to be out-of-order
@@ -512,7 +497,7 @@ describe("SlidingSyncSdk", () => {
initial: true, // e.g requested via room subscription initial: true, // e.g requested via room subscription
}); });
const gotRoom = client!.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeTruthy();
if (gotRoom == null) { if (gotRoom == null) {
return; return;
} }
@@ -530,7 +515,7 @@ describe("SlidingSyncSdk", () => {
); );
// we expect the timeline now to be oldTimeline (so the old events are in fact old) // we expect the timeline now to be oldTimeline (so the old events are in fact old)
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline); assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), oldTimeline);
}); });
}); });
}); });
@@ -626,9 +611,9 @@ describe("SlidingSyncSdk", () => {
await httpBackend!.flush("/profile", 1, 1000); await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name); await emitPromise(client!, RoomMemberEvent.Name);
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeTruthy();
const inviteeMember = room.getMember(invitee)!; const inviteeMember = room.getMember(invitee)!;
expect(inviteeMember).toBeDefined(); expect(inviteeMember).toBeTruthy();
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url); expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
expect(inviteeMember.name).toEqual(inviteeProfile.displayname); expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
}); });
@@ -723,7 +708,7 @@ describe("SlidingSyncSdk", () => {
], ],
}); });
globalData = client!.getAccountData(globalType)!; globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeDefined(); expect(globalData).toBeTruthy();
expect(globalData.getContent()).toEqual(globalContent); expect(globalData.getContent()).toEqual(globalContent);
}); });
@@ -744,6 +729,7 @@ describe("SlidingSyncSdk", () => {
foo: "bar", foo: "bar",
}; };
const roomType = "test"; const roomType = "test";
await emitPromise(client!, ClientEvent.Room);
ext.onResponse({ ext.onResponse({
rooms: { rooms: {
[roomId]: [ [roomId]: [
@@ -755,9 +741,9 @@ describe("SlidingSyncSdk", () => {
}, },
}); });
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeTruthy();
const event = room.getAccountData(roomType)!; const event = room.getAccountData(roomType)!;
expect(event).toBeDefined(); expect(event).toBeTruthy();
expect(event.getContent()).toEqual(roomContent); expect(event.getContent()).toEqual(roomContent);
}); });
@@ -943,8 +929,9 @@ describe("SlidingSyncSdk", () => {
], ],
initial: true, initial: true,
}); });
await emitPromise(client!, ClientEvent.Room);
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeTruthy();
expect(room.getMember(selfUserId)?.typing).toEqual(false); expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({ ext.onResponse({
rooms: { rooms: {
@@ -984,7 +971,7 @@ describe("SlidingSyncSdk", () => {
initial: true, initial: true,
}); });
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeTruthy();
expect(room.getMember(selfUserId)?.typing).toEqual(false); expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({ ext.onResponse({
rooms: { rooms: {
@@ -1077,12 +1064,13 @@ describe("SlidingSyncSdk", () => {
], ],
initial: true, initial: true,
}); });
await emitPromise(client!, ClientEvent.Room);
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeTruthy();
expect(room.getReadReceiptForUserId(alice, true)).toBeNull(); expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567)); ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
const receipt = room.getReadReceiptForUserId(alice); const receipt = room.getReadReceiptForUserId(alice);
expect(receipt).toBeDefined(); expect(receipt).toBeTruthy();
expect(receipt?.eventId).toEqual(lastEvent.event_id); expect(receipt?.eventId).toEqual(lastEvent.event_id);
expect(receipt?.data.ts).toEqual(1234567); expect(receipt?.data.ts).toEqual(1234567);
expect(receipt?.data.thread_id).toBeFalsy(); expect(receipt?.data.thread_id).toBeFalsy();

View File

@@ -697,7 +697,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());
room.addLiveEvents([root]); await room.addLiveEvents([root]);
// 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

@@ -172,9 +172,9 @@ describe("Room", function () {
* @param timestamp - Timestamp of the message * @param timestamp - Timestamp of the message
* @return The message event * @return The message event
*/ */
const mkMessageInRoom = (room: Room, timestamp: number) => { const mkMessageInRoom = async (room: Room, timestamp: number) => {
const message = mkMessage({ ts: timestamp }); const message = mkMessage({ ts: timestamp });
room.addLiveEvents([message]); await room.addLiveEvents([message]);
return message; return message;
}; };
@@ -319,23 +319,25 @@ describe("Room", 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", async () => {
expect(() => room.addLiveEvents(events, DuplicateStrategy.Replace, false)).not.toThrow(); await expect(room.addLiveEvents(events, DuplicateStrategy.Replace, false)).resolves.not.toThrow();
expect(() => room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).not.toThrow(); await expect(room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).resolves.not.toThrow();
// @ts-ignore await expect(
expect(() => room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false)).toThrow(); // @ts-ignore
room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false),
).rejects.toThrow();
}); });
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function () { it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", async function () {
expect(function () { return expect(
// @ts-ignore // @ts-ignore
room.addLiveEvents(events, { room.addLiveEvents(events, {
duplicateStrategy: "foo", duplicateStrategy: "foo",
}); }),
}).toThrow(); ).rejects.toThrow();
}); });
it("should replace a timeline event if dupe strategy is 'replace'", function () { it("should replace a timeline event if dupe strategy is 'replace'", async function () {
// make a duplicate // make a duplicate
const dupe = utils.mkMessage({ const dupe = utils.mkMessage({
room: roomId, room: roomId,
@@ -344,15 +346,15 @@ describe("Room", function () {
event: true, event: true,
}); });
dupe.event.event_id = events[0].getId(); dupe.event.event_id = events[0].getId();
room.addLiveEvents(events); await room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]); expect(room.timeline[0]).toEqual(events[0]);
room.addLiveEvents([dupe], { await room.addLiveEvents([dupe], {
duplicateStrategy: DuplicateStrategy.Replace, duplicateStrategy: DuplicateStrategy.Replace,
}); });
expect(room.timeline[0]).toEqual(dupe); expect(room.timeline[0]).toEqual(dupe);
}); });
it("should ignore a given dupe event if dupe strategy is 'ignore'", function () { it("should ignore a given dupe event if dupe strategy is 'ignore'", async function () {
// make a duplicate // make a duplicate
const dupe = utils.mkMessage({ const dupe = utils.mkMessage({
room: roomId, room: roomId,
@@ -361,16 +363,16 @@ describe("Room", function () {
event: true, event: true,
}); });
dupe.event.event_id = events[0].getId(); dupe.event.event_id = events[0].getId();
room.addLiveEvents(events); await room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]); expect(room.timeline[0]).toEqual(events[0]);
// @ts-ignore // @ts-ignore
room.addLiveEvents([dupe], { await room.addLiveEvents([dupe], {
duplicateStrategy: "ignore", duplicateStrategy: "ignore",
}); });
expect(room.timeline[0]).toEqual(events[0]); expect(room.timeline[0]).toEqual(events[0]);
}); });
it("should emit 'Room.timeline' events", function () { it("should emit 'Room.timeline' events", async function () {
let callCount = 0; let callCount = 0;
room.on(RoomEvent.Timeline, function (event, emitRoom, toStart) { room.on(RoomEvent.Timeline, function (event, emitRoom, toStart) {
callCount += 1; callCount += 1;
@@ -379,11 +381,11 @@ describe("Room", function () {
expect(emitRoom).toEqual(room); expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy(); expect(toStart).toBeFalsy();
}); });
room.addLiveEvents(events); await room.addLiveEvents(events);
expect(callCount).toEqual(2); expect(callCount).toEqual(2);
}); });
it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events", function () { it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events", async function () {
const events: MatrixEvent[] = [ const events: MatrixEvent[] = [
utils.mkMembership({ utils.mkMembership({
room: roomId, room: roomId,
@@ -402,7 +404,7 @@ describe("Room", function () {
}, },
}), }),
]; ];
room.addLiveEvents(events); await room.addLiveEvents(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: false }); expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: false });
expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: false }); expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: false });
expect(events[0].forwardLooking).toBe(true); expect(events[0].forwardLooking).toBe(true);
@@ -410,7 +412,7 @@ describe("Room", function () {
expect(room.oldState.setStateEvents).not.toHaveBeenCalled(); expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
}); });
it("should synthesize read receipts for the senders of events", function () { it("should synthesize read receipts for the senders of events", async function () {
const sentinel = { const sentinel = {
userId: userA, userId: userA,
membership: "join", membership: "join",
@@ -422,11 +424,11 @@ describe("Room", function () {
} }
return null; return null;
}); });
room.addLiveEvents(events); await room.addLiveEvents(events);
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId()); expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
}); });
it("should emit Room.localEchoUpdated when a local echo is updated", function () { it("should emit Room.localEchoUpdated when a local echo is updated", async function () {
const localEvent = utils.mkMessage({ const localEvent = utils.mkMessage({
room: roomId, room: roomId,
user: userA, user: userA,
@@ -457,7 +459,7 @@ describe("Room", function () {
expect(stub.mock.calls[0][3]).toBeUndefined(); expect(stub.mock.calls[0][3]).toBeUndefined();
// then the remoteEvent // then the remoteEvent
room.addLiveEvents([remoteEvent]); await room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
expect(stub).toHaveBeenCalledTimes(2); expect(stub).toHaveBeenCalledTimes(2);
@@ -469,7 +471,7 @@ describe("Room", function () {
expect(stub.mock.calls[1][3]).toBe(EventStatus.SENDING); expect(stub.mock.calls[1][3]).toBe(EventStatus.SENDING);
}); });
it("should be able to update local echo without a txn ID (/send then /sync)", function () { it("should be able to update local echo without a txn ID (/send then /sync)", async function () {
const eventJson = utils.mkMessage({ const eventJson = utils.mkMessage({
room: roomId, room: roomId,
user: userA, user: userA,
@@ -495,14 +497,14 @@ describe("Room", function () {
// then /sync returns the remoteEvent, it should de-dupe based on the event ID. // then /sync returns the remoteEvent, it should de-dupe based on the event ID.
const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson)); const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson));
expect(remoteEvent.getTxnId()).toBeUndefined(); expect(remoteEvent.getTxnId()).toBeUndefined();
room.addLiveEvents([remoteEvent]); await room.addLiveEvents([remoteEvent]);
// the duplicate strategy code should ensure we don't add a 2nd event to the live timeline // the duplicate strategy code should ensure we don't add a 2nd event to the live timeline
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
// but without the event ID matching we will still have the local event in pending events // but without the event ID matching we will still have the local event in pending events
expect(room.getEventForTxnId(txnId)).toBeUndefined(); expect(room.getEventForTxnId(txnId)).toBeUndefined();
}); });
it("should be able to update local echo without a txn ID (/sync then /send)", function () { it("should be able to update local echo without a txn ID (/sync then /send)", async function () {
const eventJson = utils.mkMessage({ const eventJson = utils.mkMessage({
room: roomId, room: roomId,
user: userA, user: userA,
@@ -525,7 +527,7 @@ describe("Room", function () {
const realEventId = "$real-event-id"; const realEventId = "$real-event-id";
const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson)); const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson));
expect(remoteEvent.getUnsigned().transaction_id).toBeUndefined(); expect(remoteEvent.getUnsigned().transaction_id).toBeUndefined();
room.addLiveEvents([remoteEvent]); await room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(2); // impossible to de-dupe as no txn ID or matching event ID expect(room.timeline.length).toEqual(2); // impossible to de-dupe as no txn ID or matching event ID
// then the /send request returns the real event ID. // then the /send request returns the real event ID.
@@ -538,7 +540,7 @@ describe("Room", function () {
expect(room.getEventForTxnId(txnId)).toBeUndefined(); expect(room.getEventForTxnId(txnId)).toBeUndefined();
}); });
it("should correctly handle remote echoes from other devices", () => { it("should correctly handle remote echoes from other devices", async () => {
const remoteEvent = utils.mkMessage({ const remoteEvent = utils.mkMessage({
room: roomId, room: roomId,
user: userA, user: userA,
@@ -547,7 +549,7 @@ describe("Room", function () {
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" }; remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
// add the remoteEvent // add the remoteEvent
room.addLiveEvents([remoteEvent]); await room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
}); });
}); });
@@ -612,7 +614,7 @@ describe("Room", function () {
}); });
describe("event metadata handling", function () { describe("event metadata handling", function () {
it("should set event.sender for new and old events", function () { it("should set event.sender for new and old events", async function () {
const sentinel = { const sentinel = {
userId: userA, userId: userA,
membership: "join", membership: "join",
@@ -650,13 +652,13 @@ describe("Room", function () {
event: true, event: true,
content: { name: "Old Room Name" }, content: { name: "Old Room Name" },
}); });
room.addLiveEvents([newEv]); await room.addLiveEvents([newEv]);
expect(newEv.sender).toEqual(sentinel); expect(newEv.sender).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline()); room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.sender).toEqual(oldSentinel); expect(oldEv.sender).toEqual(oldSentinel);
}); });
it("should set event.target for new and old m.room.member events", function () { it("should set event.target for new and old m.room.member events", async function () {
const sentinel = { const sentinel = {
userId: userA, userId: userA,
membership: "join", membership: "join",
@@ -694,7 +696,7 @@ describe("Room", function () {
skey: userA, skey: userA,
event: true, event: true,
}); });
room.addLiveEvents([newEv]); await room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel); expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline()); room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel); expect(oldEv.target).toEqual(oldSentinel);
@@ -763,12 +765,12 @@ describe("Room", function () {
]; ];
}); });
it("should copy state from previous timeline", function () { it("should copy state from previous timeline", async function () {
room.addLiveEvents([events[0], events[1]]); await room.addLiveEvents([events[0], events[1]]);
expect(room.getLiveTimeline().getEvents().length).toEqual(2); expect(room.getLiveTimeline().getEvents().length).toEqual(2);
room.resetLiveTimeline("sometoken", "someothertoken"); room.resetLiveTimeline("sometoken", "someothertoken");
room.addLiveEvents([events[2]]); await room.addLiveEvents([events[2]]);
const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
expect(room.getLiveTimeline().getEvents().length).toEqual(1); expect(room.getLiveTimeline().getEvents().length).toEqual(1);
@@ -776,8 +778,8 @@ describe("Room", function () {
expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]); expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]);
}); });
it("should reset the legacy timeline fields", function () { it("should reset the legacy timeline fields", async function () {
room.addLiveEvents([events[0], events[1]]); await room.addLiveEvents([events[0], events[1]]);
expect(room.timeline.length).toEqual(2); expect(room.timeline.length).toEqual(2);
const oldStateBeforeRunningReset = room.oldState; const oldStateBeforeRunningReset = room.oldState;
@@ -798,7 +800,7 @@ describe("Room", function () {
room.resetLiveTimeline("sometoken", "someothertoken"); room.resetLiveTimeline("sometoken", "someothertoken");
room.addLiveEvents([events[2]]); await room.addLiveEvents([events[2]]);
const newLiveTimeline = room.getLiveTimeline(); const newLiveTimeline = room.getLiveTimeline();
expect(room.timeline).toEqual(newLiveTimeline.getEvents()); expect(room.timeline).toEqual(newLiveTimeline.getEvents());
expect(room.oldState).toEqual(newLiveTimeline.getState(EventTimeline.BACKWARDS)); expect(room.oldState).toEqual(newLiveTimeline.getState(EventTimeline.BACKWARDS));
@@ -824,8 +826,8 @@ describe("Room", function () {
expect(callCount).toEqual(1); expect(callCount).toEqual(1);
}); });
it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", function () { it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", async function () {
room.addLiveEvents([events[0]]); await room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
const firstLiveTimeline = room.getLiveTimeline(); const firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline("sometoken", "someothertoken"); room.resetLiveTimeline("sometoken", "someothertoken");
@@ -868,8 +870,8 @@ describe("Room", function () {
}), }),
]; ];
it("should handle events in the same timeline", function () { it("should handle events in the same timeline", async function () {
room.addLiveEvents(events); await room.addLiveEvents(events);
expect( expect(
room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!), room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!),
@@ -882,13 +884,13 @@ describe("Room", function () {
).toEqual(0); ).toEqual(0);
}); });
it("should handle events in adjacent timelines", function () { it("should handle events in adjacent timelines", async function () {
const oldTimeline = room.addTimeline(); const oldTimeline = room.addTimeline();
oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward); oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward);
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward); room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward);
room.addEventsToTimeline([events[0]], false, oldTimeline); room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]); await room.addLiveEvents([events[1]]);
expect( expect(
room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!), room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!),
@@ -898,11 +900,11 @@ describe("Room", function () {
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
it("should return null for events in non-adjacent timelines", function () { it("should return null for events in non-adjacent timelines", async function () {
const oldTimeline = room.addTimeline(); const oldTimeline = room.addTimeline();
room.addEventsToTimeline([events[0]], false, oldTimeline); room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]); await room.addLiveEvents([events[1]]);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)).toBe( expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)).toBe(
null, null,
@@ -912,8 +914,8 @@ describe("Room", function () {
); );
}); });
it("should return null for unknown events", function () { it("should return null for unknown events", async function () {
room.addLiveEvents(events); await room.addLiveEvents(events);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, "xxx")).toBe(null); expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, "xxx")).toBe(null);
expect(room.getUnfilteredTimelineSet().compareEventOrdering("xxx", events[0].getId()!)).toBe(null); expect(room.getUnfilteredTimelineSet().compareEventOrdering("xxx", events[0].getId()!)).toBe(null);
@@ -990,8 +992,8 @@ describe("Room", function () {
}); });
describe("recalculate", function () { describe("recalculate", function () {
const setJoinRule = function (rule: JoinRule) { const setJoinRule = async function (rule: JoinRule) {
room.addLiveEvents([ await room.addLiveEvents([
utils.mkEvent({ utils.mkEvent({
type: EventType.RoomJoinRules, type: EventType.RoomJoinRules,
room: roomId, room: roomId,
@@ -1003,8 +1005,8 @@ describe("Room", function () {
}), }),
]); ]);
}; };
const setAltAliases = function (aliases: string[]) { const setAltAliases = async function (aliases: string[]) {
room.addLiveEvents([ await room.addLiveEvents([
utils.mkEvent({ utils.mkEvent({
type: EventType.RoomCanonicalAlias, type: EventType.RoomCanonicalAlias,
room: roomId, room: roomId,
@@ -1016,8 +1018,8 @@ describe("Room", function () {
}), }),
]); ]);
}; };
const setAlias = function (alias: string) { const setAlias = async function (alias: string) {
room.addLiveEvents([ await room.addLiveEvents([
utils.mkEvent({ utils.mkEvent({
type: EventType.RoomCanonicalAlias, type: EventType.RoomCanonicalAlias,
room: roomId, room: roomId,
@@ -1027,8 +1029,8 @@ describe("Room", function () {
}), }),
]); ]);
}; };
const setRoomName = function (name: string) { const setRoomName = async function (name: string) {
room.addLiveEvents([ await room.addLiveEvents([
utils.mkEvent({ utils.mkEvent({
type: EventType.RoomName, type: EventType.RoomName,
room: roomId, room: roomId,
@@ -1040,14 +1042,14 @@ describe("Room", function () {
}), }),
]); ]);
}; };
const addMember = function (userId: string, state = "join", opts: any = {}) { const addMember = async function (userId: string, state = "join", opts: any = {}) {
opts.room = roomId; opts.room = roomId;
opts.mship = state; opts.mship = state;
opts.user = opts.user || userId; opts.user = opts.user || userId;
opts.skey = userId; opts.skey = userId;
opts.event = true; opts.event = true;
const event = utils.mkMembership(opts); const event = utils.mkMembership(opts);
room.addLiveEvents([event]); await room.addLiveEvents([event]);
return event; return event;
}; };
@@ -1059,10 +1061,10 @@ describe("Room", function () {
describe("Room.recalculate => Stripped State Events", function () { describe("Room.recalculate => Stripped State Events", function () {
it( it(
"should set stripped state events as actual state events if the " + "room is an invite room", "should set stripped state events as actual state events if the " + "room is an invite room",
function () { async function () {
const roomName = "flibble"; const roomName = "flibble";
const event = addMember(userA, "invite"); const event = await addMember(userA, "invite");
event.event.unsigned = {}; event.event.unsigned = {};
event.event.unsigned.invite_room_state = [ event.event.unsigned.invite_room_state = [
{ {
@@ -1080,8 +1082,8 @@ describe("Room", function () {
}, },
); );
it("should not clobber state events if it isn't an invite room", function () { it("should not clobber state events if it isn't an invite room", async function () {
const event = addMember(userA, "join"); const event = await addMember(userA, "join");
const roomName = "flibble"; const roomName = "flibble";
setRoomName(roomName); setRoomName(roomName);
const roomNameToIgnore = "ignoreme"; const roomNameToIgnore = "ignoreme";
@@ -1537,7 +1539,7 @@ describe("Room", function () {
]); ]);
}); });
it("should prioritise the most recent event", function () { it("should prioritise the most recent event", async function () {
const events: MatrixEvent[] = [ const events: MatrixEvent[] = [
utils.mkMessage({ utils.mkMessage({
room: roomId, room: roomId,
@@ -1559,7 +1561,7 @@ describe("Room", function () {
}), }),
]; ];
room.addLiveEvents(events); await room.addLiveEvents(events);
const ts = 13787898424; const ts = 13787898424;
// check it initialises correctly // check it initialises correctly
@@ -1575,7 +1577,7 @@ describe("Room", function () {
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId()); expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
}); });
it("should prioritise the most recent event even if it is synthetic", () => { it("should prioritise the most recent event even if it is synthetic", async () => {
const events: MatrixEvent[] = [ const events: MatrixEvent[] = [
utils.mkMessage({ utils.mkMessage({
room: roomId, room: roomId,
@@ -1597,7 +1599,7 @@ describe("Room", function () {
}), }),
]; ];
room.addLiveEvents(events); await room.addLiveEvents(events);
const ts = 13787898424; const ts = 13787898424;
// check it initialises correctly // check it initialises correctly
@@ -1673,66 +1675,72 @@ describe("Room", function () {
}); });
describe("addPendingEvent", function () { describe("addPendingEvent", function () {
it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function () { it(
const client = new TestClient("@alice:example.com", "alicedevice").client; "should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'",
client.supportsThreads = () => true; async function () {
const room = new Room(roomId, client, userA, { const client = new TestClient("@alice:example.com", "alicedevice").client;
pendingEventOrdering: PendingEventOrdering.Detached, client.supportsThreads = () => true;
}); const room = new Room(roomId, client, userA, {
const eventA = utils.mkMessage({ pendingEventOrdering: PendingEventOrdering.Detached,
room: roomId, });
user: userA, const eventA = utils.mkMessage({
msg: "remote 1", room: roomId,
event: true, user: userA,
}); msg: "remote 1",
const eventB = utils.mkMessage({ event: true,
room: roomId, });
user: userA, const eventB = utils.mkMessage({
msg: "local 1", room: roomId,
event: true, user: userA,
}); msg: "local 1",
eventB.status = EventStatus.SENDING; event: true,
const eventC = utils.mkMessage({ });
room: roomId, eventB.status = EventStatus.SENDING;
user: userA, const eventC = utils.mkMessage({
msg: "remote 2", room: roomId,
event: true, user: userA,
}); msg: "remote 2",
room.addLiveEvents([eventA]); event: true,
room.addPendingEvent(eventB, "TXN1"); });
room.addLiveEvents([eventC]); await room.addLiveEvents([eventA]);
expect(room.timeline).toEqual([eventA, eventC]); room.addPendingEvent(eventB, "TXN1");
expect(room.getPendingEvents()).toEqual([eventB]); await room.addLiveEvents([eventC]);
}); expect(room.timeline).toEqual([eventA, eventC]);
expect(room.getPendingEvents()).toEqual([eventB]);
},
);
it("should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'", function () { it(
const room = new Room(roomId, new TestClient(userA).client, userA, { "should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'",
pendingEventOrdering: PendingEventOrdering.Chronological, async function () {
}); const room = new Room(roomId, new TestClient(userA).client, userA, {
const eventA = utils.mkMessage({ pendingEventOrdering: PendingEventOrdering.Chronological,
room: roomId, });
user: userA, const eventA = utils.mkMessage({
msg: "remote 1", room: roomId,
event: true, user: userA,
}); msg: "remote 1",
const eventB = utils.mkMessage({ event: true,
room: roomId, });
user: userA, const eventB = utils.mkMessage({
msg: "local 1", room: roomId,
event: true, user: userA,
}); msg: "local 1",
eventB.status = EventStatus.SENDING; event: true,
const eventC = utils.mkMessage({ });
room: roomId, eventB.status = EventStatus.SENDING;
user: userA, const eventC = utils.mkMessage({
msg: "remote 2", room: roomId,
event: true, user: userA,
}); msg: "remote 2",
room.addLiveEvents([eventA]); event: true,
room.addPendingEvent(eventB, "TXN1"); });
room.addLiveEvents([eventC]); await room.addLiveEvents([eventA]);
expect(room.timeline).toEqual([eventA, eventB, eventC]); room.addPendingEvent(eventB, "TXN1");
}); await room.addLiveEvents([eventC]);
expect(room.timeline).toEqual([eventA, eventB, eventC]);
},
);
it("should apply redactions eagerly in the pending event list", () => { it("should apply redactions eagerly in the pending event list", () => {
const client = new TestClient("@alice:example.com", "alicedevice").client; const client = new TestClient("@alice:example.com", "alicedevice").client;
@@ -2004,9 +2012,9 @@ describe("Room", function () {
}); });
expect(room.guessDMUserId()).toEqual(userB); expect(room.guessDMUserId()).toEqual(userB);
}); });
it("should return first member that isn't self", function () { it("should return first member that isn't self", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userB, user: userB,
mship: "join", mship: "join",
@@ -2070,9 +2078,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
}); });
it("should return a display name if one other member is in the room", function () { it("should return a display name if one other member is in the room", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2091,9 +2099,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("User B"); expect(room.getDefaultRoomName(userA)).toEqual("User B");
}); });
it("should return a display name if one other member is banned", function () { it("should return a display name if one other member is banned", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2112,9 +2120,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)"); expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
}); });
it("should return a display name if one other member is invited", function () { it("should return a display name if one other member is invited", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2133,9 +2141,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("User B"); expect(room.getDefaultRoomName(userA)).toEqual("User B");
}); });
it("should return 'Empty room (was User B)' if User B left the room", function () { it("should return 'Empty room (was User B)' if User B left the room", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2154,9 +2162,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)"); expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
}); });
it("should return 'User B and User C' if in a room with two other users", function () { it("should return 'User B and User C' if in a room with two other users", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2182,9 +2190,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("User B and User C"); expect(room.getDefaultRoomName(userA)).toEqual("User B and User C");
}); });
it("should return 'User B and 2 others' if in a room with three other users", function () { it("should return 'User B and 2 others' if in a room with three other users", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2219,9 +2227,9 @@ describe("Room", function () {
}); });
describe("io.element.functional_users", function () { describe("io.element.functional_users", function () {
it("should return a display name (default behaviour) if no one is marked as a functional member", function () { it("should return a display name (default behaviour) if no one is marked as a functional member", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2249,9 +2257,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("User B"); expect(room.getDefaultRoomName(userA)).toEqual("User B");
}); });
it("should return a display name (default behaviour) if service members is a number (invalid)", function () { it("should return a display name (default behaviour) if service members is a number (invalid)", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2279,9 +2287,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("User B"); expect(room.getDefaultRoomName(userA)).toEqual("User B");
}); });
it("should return a display name (default behaviour) if service members is a string (invalid)", function () { it("should return a display name (default behaviour) if service members is a string (invalid)", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2309,9 +2317,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("User B"); expect(room.getDefaultRoomName(userA)).toEqual("User B");
}); });
it("should return 'Empty room' if the only other member is a functional member", function () { it("should return 'Empty room' if the only other member is a functional member", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2339,9 +2347,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
}); });
it("should return 'User B' if User B is the only other member who isn't a functional member", function () { it("should return 'User B' if User B is the only other member who isn't a functional member", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2377,9 +2385,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("User B"); expect(room.getDefaultRoomName(userA)).toEqual("User B");
}); });
it("should return 'Empty room' if all other members are functional members", function () { it("should return 'Empty room' if all other members are functional members", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2415,9 +2423,9 @@ describe("Room", function () {
expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
}); });
it("should not break if an unjoined user is marked as a service user", function () { it("should not break if an unjoined user is marked as a service user", async function () {
const room = new Room(roomId, new TestClient(userA).client, userA); const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([ await room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userA, user: userA,
mship: "join", mship: "join",
@@ -2548,7 +2556,7 @@ describe("Room", function () {
}); });
let prom = emitPromise(room, ThreadEvent.New); let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([randomMessage, threadRoot, threadResponse]); await room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
const thread: Thread = await prom; const thread: Thread = await prom;
await emitPromise(room, ThreadEvent.Update); await emitPromise(room, ThreadEvent.Update);
@@ -2575,7 +2583,7 @@ describe("Room", function () {
}); });
prom = emitPromise(room, ThreadEvent.Update); prom = emitPromise(room, ThreadEvent.Update);
room.addLiveEvents([threadResponseEdit]); await room.addLiveEvents([threadResponseEdit]);
await prom; await prom;
expect(thread.replyToEvent!.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body); expect(thread.replyToEvent!.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
}); });
@@ -2606,7 +2614,7 @@ describe("Room", function () {
}); });
let prom = emitPromise(room, ThreadEvent.New); let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]); await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await prom; const thread = await prom;
await emitPromise(room, ThreadEvent.Update); await emitPromise(room, ThreadEvent.Update);
@@ -2641,7 +2649,7 @@ describe("Room", function () {
prom = emitPromise(thread, ThreadEvent.Update); prom = emitPromise(thread, ThreadEvent.Update);
const threadResponse1Redaction = mkRedaction(threadResponse1); const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]); await room.addLiveEvents([threadResponse1Redaction]);
await prom; await prom;
expect(thread).toHaveLength(1); expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId()); expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
@@ -2674,7 +2682,7 @@ describe("Room", function () {
}); });
const prom = emitPromise(room, ThreadEvent.New); const prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]); await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await prom; const thread = await prom;
await emitPromise(room, ThreadEvent.Update); await emitPromise(room, ThreadEvent.Update);
@@ -2682,7 +2690,7 @@ describe("Room", function () {
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId()); expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction); const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
room.addLiveEvents([threadResponse2ReactionRedaction]); await room.addLiveEvents([threadResponse2ReactionRedaction]);
expect(thread).toHaveLength(2); expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId()); expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
}); });
@@ -2714,7 +2722,7 @@ describe("Room", function () {
}); });
let prom = emitPromise(room, ThreadEvent.New); let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]); await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await prom; const thread = await prom;
await emitPromise(room, ThreadEvent.Update); await emitPromise(room, ThreadEvent.Update);
@@ -2723,7 +2731,7 @@ describe("Room", function () {
prom = emitPromise(room, ThreadEvent.Update); prom = emitPromise(room, ThreadEvent.Update);
const threadRootRedaction = mkRedaction(threadRoot); const threadRootRedaction = mkRedaction(threadRoot);
room.addLiveEvents([threadRootRedaction]); await room.addLiveEvents([threadRootRedaction]);
await prom; await prom;
expect(thread).toHaveLength(2); expect(thread).toHaveLength(2);
}); });
@@ -2776,12 +2784,12 @@ describe("Room", function () {
}); });
let prom = emitPromise(room, ThreadEvent.New); let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1]); await room.addLiveEvents([threadRoot, threadResponse1]);
const thread: Thread = await prom; const thread: Thread = await prom;
await emitPromise(room, ThreadEvent.Update); await emitPromise(room, ThreadEvent.Update);
expect(thread.initialEventsFetched).toBeTruthy(); expect(thread.initialEventsFetched).toBeTruthy();
room.addLiveEvents([threadResponse2]); await room.addLiveEvents([threadResponse2]);
expect(thread).toHaveLength(2); expect(thread).toHaveLength(2);
expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId()); expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId());
@@ -2802,7 +2810,7 @@ describe("Room", function () {
prom = emitPromise(room, ThreadEvent.Update); prom = emitPromise(room, ThreadEvent.Update);
const threadResponse2Redaction = mkRedaction(threadResponse2); const threadResponse2Redaction = mkRedaction(threadResponse2);
room.addLiveEvents([threadResponse2Redaction]); await room.addLiveEvents([threadResponse2Redaction]);
await prom; await prom;
await emitPromise(room, ThreadEvent.Update); await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(1); expect(thread).toHaveLength(1);
@@ -2826,7 +2834,7 @@ describe("Room", function () {
prom = emitPromise(room, ThreadEvent.Delete); prom = emitPromise(room, ThreadEvent.Delete);
const prom2 = emitPromise(room, RoomEvent.Timeline); const prom2 = emitPromise(room, RoomEvent.Timeline);
const threadResponse1Redaction = mkRedaction(threadResponse1); const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]); await room.addLiveEvents([threadResponse1Redaction]);
await prom; await prom;
await prom2; await prom2;
expect(thread).toHaveLength(0); expect(thread).toHaveLength(0);
@@ -2946,7 +2954,7 @@ describe("Room", function () {
const events = [threadRoot, rootReaction, threadResponse, threadReaction]; const events = [threadRoot, rootReaction, threadResponse, threadReaction];
const prom = emitPromise(room, ThreadEvent.New); const prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents(events); await room.addLiveEvents(events);
const thread = await prom; const thread = await prom;
expect(thread).toBe(threadRoot.getThread()); expect(thread).toBe(threadRoot.getThread());
expect(thread.rootEvent).toBe(threadRoot); expect(thread.rootEvent).toBe(threadRoot);
@@ -3452,21 +3460,21 @@ describe("Room", function () {
expect(room.findPredecessor()).toBeNull(); expect(room.findPredecessor()).toBeNull();
}); });
it("Returns null if the create event has no predecessor", () => { it("Returns null if the create event has no predecessor", async () => {
const room = new Room("roomid", client!, "@u:example.com"); const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([roomCreateEvent("roomid", null)]); await room.addLiveEvents([roomCreateEvent("roomid", null)]);
expect(room.findPredecessor()).toBeNull(); expect(room.findPredecessor()).toBeNull();
}); });
it("Returns the predecessor ID if one is provided via create event", () => { it("Returns the predecessor ID if one is provided via create event", async () => {
const room = new Room("roomid", client!, "@u:example.com"); const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); await room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]);
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" }); expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
}); });
it("Prefers the m.predecessor event if one exists", () => { it("Prefers the m.predecessor event if one exists", async () => {
const room = new Room("roomid", client!, "@u:example.com"); const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([ await room.addLiveEvents([
roomCreateEvent("roomid", "replacedroomid"), roomCreateEvent("roomid", "replacedroomid"),
predecessorEvent("roomid", "otherreplacedroomid"), predecessorEvent("roomid", "otherreplacedroomid"),
]); ]);
@@ -3478,9 +3486,9 @@ describe("Room", function () {
}); });
}); });
it("uses the m.predecessor event ID if provided", () => { it("uses the m.predecessor event ID if provided", async () => {
const room = new Room("roomid", client!, "@u:example.com"); const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([ await room.addLiveEvents([
roomCreateEvent("roomid", "replacedroomid"), roomCreateEvent("roomid", "replacedroomid"),
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid", ["one.example.com", "two.example.com"]), predecessorEvent("roomid", "otherreplacedroomid", "lstevtid", ["one.example.com", "two.example.com"]),
]); ]);
@@ -3492,9 +3500,9 @@ describe("Room", function () {
}); });
}); });
it("Ignores the m.predecessor event if we don't ask to use it", () => { it("Ignores the m.predecessor event if we don't ask to use it", async () => {
const room = new Room("roomid", client!, "@u:example.com"); const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([ await room.addLiveEvents([
roomCreateEvent("roomid", "replacedroomid"), roomCreateEvent("roomid", "replacedroomid"),
predecessorEvent("roomid", "otherreplacedroomid"), predecessorEvent("roomid", "otherreplacedroomid"),
]); ]);
@@ -3503,9 +3511,9 @@ describe("Room", function () {
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" }); expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
}); });
it("Ignores the m.predecessor event and returns null if we don't ask to use it", () => { it("Ignores the m.predecessor event and returns null if we don't ask to use it", async () => {
const room = new Room("roomid", client!, "@u:example.com"); const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([ await room.addLiveEvents([
roomCreateEvent("roomid", null), // Create event has no predecessor roomCreateEvent("roomid", null), // Create event has no predecessor
predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"), predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"),
]); ]);
@@ -3520,8 +3528,8 @@ describe("Room", function () {
expect(room.getLastLiveEvent()).toBeUndefined(); expect(room.getLastLiveEvent()).toBeUndefined();
}); });
it("when there is only an event in the main timeline and there are no threads, it should return the last event from the main timeline", () => { it("when there is only an event in the main timeline and there are no threads, it should return the last event from the main timeline", async () => {
const lastEventInMainTimeline = mkMessageInRoom(room, 23); const lastEventInMainTimeline = await mkMessageInRoom(room, 23);
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline); expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
}); });
@@ -3536,29 +3544,29 @@ describe("Room", function () {
}); });
describe("when there are events in both, the main timeline and threads", () => { describe("when there are events in both, the main timeline and threads", () => {
it("and the last event is in a thread, it should return the last event from the thread", () => { it("and the last event is in a thread, it should return the last event from the thread", async () => {
mkMessageInRoom(room, 23); await mkMessageInRoom(room, 23);
const { thread } = mkThread({ room, length: 0 }); const { thread } = mkThread({ room, length: 0 });
const lastEventInThread = mkMessageInThread(thread, 42); const lastEventInThread = mkMessageInThread(thread, 42);
expect(room.getLastLiveEvent()).toBe(lastEventInThread); expect(room.getLastLiveEvent()).toBe(lastEventInThread);
}); });
it("and the last event is in the main timeline, it should return the last event from the main timeline", () => { it("and the last event is in the main timeline, it should return the last event from the main timeline", async () => {
const lastEventInMainTimeline = mkMessageInRoom(room, 42); const lastEventInMainTimeline = await mkMessageInRoom(room, 42);
const { thread } = mkThread({ room, length: 0 }); const { thread } = mkThread({ room, length: 0 });
mkMessageInThread(thread, 23); mkMessageInThread(thread, 23);
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline); expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
}); });
it("and both events have the same timestamp, it should return the last event from the thread", () => { it("and both events have the same timestamp, it should return the last event from the thread", async () => {
mkMessageInRoom(room, 23); await mkMessageInRoom(room, 23);
const { thread } = mkThread({ room, length: 0 }); const { thread } = mkThread({ room, length: 0 });
const lastEventInThread = mkMessageInThread(thread, 23); const lastEventInThread = mkMessageInThread(thread, 23);
expect(room.getLastLiveEvent()).toBe(lastEventInThread); expect(room.getLastLiveEvent()).toBe(lastEventInThread);
}); });
it("and there is a thread without any messages, it should return the last event from the main timeline", () => { it("and there is a thread without any messages, it should return the last event from the main timeline", async () => {
const lastEventInMainTimeline = mkMessageInRoom(room, 23); const lastEventInMainTimeline = await mkMessageInRoom(room, 23);
mkThread({ room, length: 0 }); mkThread({ room, length: 0 });
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline); expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
}); });

View File

@@ -235,6 +235,13 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
"org.matrix.msc3890.local_notification_settings", "org.matrix.msc3890.local_notification_settings",
); );
/**
* https://github.com/matrix-org/matrix-doc/pull/4023
*
* @experimental
*/
export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id");
export interface IEncryptedFile { export interface IEncryptedFile {
url: string; url: string;
mimetype?: string; mimetype?: string;

View File

@@ -5573,11 +5573,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.currentState.setUnknownStateEvents(stateEvents); room.currentState.setUnknownStateEvents(stateEvents);
} }
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents); const [timelineEvents, threadedEvents, unknownRelations] =
room.partitionThreadedEvents(matrixEvents);
this.processAggregatedTimelineEvents(room, timelineEvents); this.processAggregatedTimelineEvents(room, timelineEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline()); room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
this.processThreadEvents(room, threadedEvents, true); this.processThreadEvents(room, threadedEvents, true);
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
room.oldState.paginationToken = res.end ?? null; room.oldState.paginationToken = res.end ?? null;
if (res.chunk.length === 0) { if (res.chunk.length === 0) {
@@ -5686,11 +5688,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end; timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end;
} }
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events); const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start); timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it. // 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);
unknownRelations.forEach((event) => timelineSet.relations.aggregateChildEvent(event));
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
@@ -6230,7 +6233,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();
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents); const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token); timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
this.processAggregatedTimelineEvents(room, timelineEvents); this.processAggregatedTimelineEvents(room, timelineEvents);
this.processThreadRoots( this.processThreadRoots(
@@ -6238,6 +6241,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)), timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)),
false, false,
); );
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
const atEnd = res.end === undefined || res.end === res.start; const atEnd = res.end === undefined || res.end === res.start;

View File

@@ -24,7 +24,13 @@ import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
import type { IEventDecryptionResult } from "../@types/crypto"; import type { IEventDecryptionResult } from "../@types/crypto";
import { logger } from "../logger"; import { logger } from "../logger";
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event"; import {
EVENT_VISIBILITY_CHANGE_TYPE,
EventType,
MsgType,
RelationType,
UNSIGNED_THREAD_ID_FIELD,
} from "../@types/event";
import { Crypto } from "../crypto"; import { Crypto } from "../crypto";
import { deepSortedObjectEntries, internaliseString } from "../utils"; import { deepSortedObjectEntries, internaliseString } from "../utils";
import { RoomMember } from "./room-member"; import { RoomMember } from "./room-member";
@@ -63,6 +69,7 @@ export interface IUnsigned {
"transaction_id"?: string; "transaction_id"?: string;
"invite_room_state"?: StrippedState[]; "invite_room_state"?: StrippedState[];
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations "m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
[UNSIGNED_THREAD_ID_FIELD.name]?: string;
} }
export interface IThreadBundledRelationship { export interface IThreadBundledRelationship {

View File

@@ -39,6 +39,7 @@ import {
UNSTABLE_ELEMENT_FUNCTIONAL_USERS, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE, EVENT_VISIBILITY_CHANGE_TYPE,
RelationType, RelationType,
UNSIGNED_THREAD_ID_FIELD,
} from "../@types/event"; } from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client"; import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials"; import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
@@ -2132,6 +2133,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
return this.eventShouldLiveIn(parentEvent, events, roots); return this.eventShouldLiveIn(parentEvent, events, roots);
} }
if (!event.isRelation()) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
// Edge case where we know the event is a relation but don't have the parentEvent // Edge case where we know the event is a relation but don't have the parentEvent
if (roots?.has(event.relationEventId!)) { if (roots?.has(event.relationEventId!)) {
return { return {
@@ -2141,9 +2149,20 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}; };
} }
// We've exhausted all scenarios, can safely assume that this event should live in the room timeline only const unsigned = event.getUnsigned();
if (typeof unsigned[UNSIGNED_THREAD_ID_FIELD.name] === "string") {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: unsigned[UNSIGNED_THREAD_ID_FIELD.name],
};
}
// We've exhausted all scenarios,
// we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread
// adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts
return { return {
shouldLiveInRoom: true, shouldLiveInRoom: false,
shouldLiveInThread: false, shouldLiveInThread: false,
}; };
} }
@@ -2156,14 +2175,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
} }
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void { private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
let thread = this.getThread(threadId); const thread = this.getThread(threadId);
if (thread) {
if (!thread) { thread.addEvents(events, toStartOfTimeline);
} else {
const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId); const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId);
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline); this.createThread(threadId, rootEvent, events, toStartOfTimeline);
} }
thread.addEvents(events, toStartOfTimeline);
} }
/** /**
@@ -2700,16 +2718,20 @@ 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 addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void; public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void>;
/** /**
* @deprecated In favor of the overload with `IAddLiveEventOptions` * @deprecated In favor of the overload with `IAddLiveEventOptions`
*/ */
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache?: boolean): void; public async addLiveEvents(
public addLiveEvents( events: MatrixEvent[],
duplicateStrategy?: DuplicateStrategy,
fromCache?: boolean,
): Promise<void>;
public async addLiveEvents(
events: MatrixEvent[], events: MatrixEvent[],
duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions, duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
fromCache = false, fromCache = false,
): void { ): Promise<void> {
let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy; let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy;
let timelineWasEmpty: boolean | undefined = false; let timelineWasEmpty: boolean | undefined = false;
if (typeof duplicateStrategyOrOpts === "object") { if (typeof duplicateStrategyOrOpts === "object") {
@@ -2760,6 +2782,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
timelineWasEmpty, timelineWasEmpty,
}; };
// List of extra events to check for being parents of any relations encountered
const neighbouringEvents = [...events];
for (const event of events) { for (const event of events) {
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline". // TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
this.processLiveEvent(event); this.processLiveEvent(event);
@@ -2773,12 +2798,35 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
} }
} }
const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( let { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
event, event,
events, neighbouringEvents,
threadRoots, threadRoots,
); );
if (!shouldLiveInThread && !shouldLiveInRoom && event.isRelation()) {
try {
const parentEvent = new MatrixEvent(
await this.client.fetchRoomEvent(this.roomId, event.relationEventId!),
);
neighbouringEvents.push(parentEvent);
if (parentEvent.threadRootId) {
threadRoots.add(parentEvent.threadRootId);
const unsigned = event.getUnsigned();
unsigned[UNSIGNED_THREAD_ID_FIELD.name] = parentEvent.threadRootId;
event.setUnsigned(unsigned);
}
({ shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
event,
neighbouringEvents,
threadRoots,
));
} catch (e) {
logger.error("Failed to load parent event of unhandled relation", e);
}
}
if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) { if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) {
eventsByThread[threadId ?? ""] = []; eventsByThread[threadId ?? ""] = [];
} }
@@ -2786,6 +2834,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (shouldLiveInRoom) { if (shouldLiveInRoom) {
this.addLiveEvent(event, options); this.addLiveEvent(event, options);
} else if (!shouldLiveInThread && event.isRelation()) {
this.relations.aggregateChildEvent(event);
} }
} }
@@ -2796,13 +2846,14 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
public partitionThreadedEvents( public partitionThreadedEvents(
events: MatrixEvent[], events: MatrixEvent[],
): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[]] { ): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[], unknownRelations: MatrixEvent[]] {
// Indices to the events array, for readability // Indices to the events array, for readability
const ROOM = 0; const ROOM = 0;
const THREAD = 1; const THREAD = 1;
const UNKNOWN_RELATION = 2;
if (this.client.supportsThreads()) { if (this.client.supportsThreads()) {
const threadRoots = this.findThreadRoots(events); const threadRoots = this.findThreadRoots(events);
return events.reduce( return events.reduce<[MatrixEvent[], MatrixEvent[], MatrixEvent[]]>(
(memo, event: MatrixEvent) => { (memo, event: MatrixEvent) => {
const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
event, event,
@@ -2819,13 +2870,17 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
memo[THREAD].push(event); memo[THREAD].push(event);
} }
if (!shouldLiveInThread && !shouldLiveInRoom) {
memo[UNKNOWN_RELATION].push(event);
}
return memo; return memo;
}, },
[[] as MatrixEvent[], [] as MatrixEvent[]], [[], [], []],
); );
} else { } else {
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents // When `experimentalThreadSupport` is disabled treat all events as timelineEvents
return [events as MatrixEvent[], [] as MatrixEvent[]]; return [events as MatrixEvent[], [] as MatrixEvent[], [] as MatrixEvent[]];
} }
} }
@@ -2838,6 +2893,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (event.isRelation(THREAD_RELATION_TYPE.name)) { if (event.isRelation(THREAD_RELATION_TYPE.name)) {
threadRoots.add(event.relationEventId ?? ""); threadRoots.add(event.relationEventId ?? "");
} }
const unsigned = event.getUnsigned();
if (typeof unsigned[UNSIGNED_THREAD_ID_FIELD.name] === "string") {
threadRoots.add(unsigned[UNSIGNED_THREAD_ID_FIELD.name]!);
}
} }
return threadRoots; return threadRoots;
} }

View File

@@ -628,7 +628,7 @@ export class SlidingSyncSdk {
if (roomData.invite_state) { if (roomData.invite_state) {
const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state); const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state);
this.injectRoomEvents(room, inviteStateEvents); await this.injectRoomEvents(room, inviteStateEvents);
if (roomData.initial) { if (roomData.initial) {
room.recalculate(); room.recalculate();
this.client.store.storeRoom(room); this.client.store.storeRoom(room);
@@ -700,7 +700,7 @@ export class SlidingSyncSdk {
} }
} */ } */
this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live); await this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live);
// we deliberately don't add ephemeral events to the timeline // we deliberately don't add ephemeral events to the timeline
room.addEphemeralEvents(ephemeralEvents); room.addEphemeralEvents(ephemeralEvents);
@@ -747,12 +747,12 @@ export class SlidingSyncSdk {
* @param numLive - the number of events in timelineEventList which just happened, * @param numLive - the number of events in timelineEventList which just happened,
* supplied from the server. * supplied from the server.
*/ */
public injectRoomEvents( public async injectRoomEvents(
room: Room, room: Room,
stateEventList: MatrixEvent[], stateEventList: MatrixEvent[],
timelineEventList?: MatrixEvent[], timelineEventList?: MatrixEvent[],
numLive?: number, numLive?: number,
): void { ): Promise<void> {
timelineEventList = timelineEventList || []; timelineEventList = timelineEventList || [];
stateEventList = stateEventList || []; stateEventList = stateEventList || [];
numLive = numLive || 0; numLive = numLive || 0;
@@ -811,11 +811,11 @@ export class SlidingSyncSdk {
// if the timeline has any state events in it. // 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.
room.addLiveEvents(timelineEventList, { await room.addLiveEvents(timelineEventList, {
fromCache: true, fromCache: true,
}); });
if (liveTimelineEvents.length > 0) { if (liveTimelineEvents.length > 0) {
room.addLiveEvents(liveTimelineEvents, { await room.addLiveEvents(liveTimelineEvents, {
fromCache: false, fromCache: false,
}); });
} }

View File

@@ -501,7 +501,7 @@ export class SyncApi {
}, },
) )
.then( .then(
(res) => { async (res) => {
if (this._peekRoom !== peekRoom) { if (this._peekRoom !== peekRoom) {
debuglog("Stopped peeking in room %s", peekRoom.roomId); debuglog("Stopped peeking in room %s", peekRoom.roomId);
return; return;
@@ -541,7 +541,7 @@ export class SyncApi {
}) })
.map(this.client.getEventMapper()); .map(this.client.getEventMapper());
peekRoom.addLiveEvents(events); await peekRoom.addLiveEvents(events);
this.peekPoll(peekRoom, res.end); this.peekPoll(peekRoom, res.end);
}, },
(err) => { (err) => {
@@ -899,8 +899,6 @@ export class SyncApi {
// Reset after a successful sync // Reset after a successful sync
this.failedSyncCount = 0; this.failedSyncCount = 0;
await this.client.store.setSyncData(data);
const syncEventData = { const syncEventData = {
oldSyncToken: syncToken ?? undefined, oldSyncToken: syncToken ?? undefined,
nextSyncToken: data.next_batch, nextSyncToken: data.next_batch,
@@ -924,6 +922,10 @@ export class SyncApi {
this.client.emit(ClientEvent.SyncUnexpectedError, <Error>e); this.client.emit(ClientEvent.SyncUnexpectedError, <Error>e);
} }
// Persist after processing as `unsigned` may get mutated
// with an `org.matrix.msc4023.thread_id`
await this.client.store.setSyncData(data);
// update this as it may have changed // update this as it may have changed
syncEventData.catchingUp = this.catchingUp; syncEventData.catchingUp = this.catchingUp;
@@ -1627,16 +1629,17 @@ export class SyncApi {
return Object.keys(obj) return Object.keys(obj)
.filter((k) => !unsafeProp(k)) .filter((k) => !unsafeProp(k))
.map((roomId) => { .map((roomId) => {
const arrObj = obj[roomId] as T & { room: Room; isBrandNewRoom: boolean };
let room = client.store.getRoom(roomId); let room = client.store.getRoom(roomId);
let isBrandNewRoom = false; let isBrandNewRoom = false;
if (!room) { if (!room) {
room = this.createRoom(roomId); room = this.createRoom(roomId);
isBrandNewRoom = true; isBrandNewRoom = true;
} }
arrObj.room = room; return {
arrObj.isBrandNewRoom = isBrandNewRoom; ...obj[roomId],
return arrObj; room,
isBrandNewRoom,
};
}); });
} }
@@ -1773,7 +1776,7 @@ export class SyncApi {
// if the timeline has any state events in it. // 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.
room.addLiveEvents(timelineEventList || [], { await room.addLiveEvents(timelineEventList || [], {
fromCache, fromCache,
timelineWasEmpty, timelineWasEmpty,
}); });