You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +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:
committed by
GitHub
parent
9c5c7ddb17
commit
71f9b25db7
@ -1142,7 +1142,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
@ -1156,7 +1156,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// 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!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
@ -1248,7 +1248,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
@ -1267,7 +1267,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// 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!)!;
|
||||
// then the events should still be all in the right order
|
||||
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(THREAD2_ROOT);
|
||||
room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
@ -1937,11 +1937,6 @@ describe("MatrixClient event timelines", function () {
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
|
@ -36,11 +36,15 @@ import {
|
||||
NotificationCountType,
|
||||
IEphemeral,
|
||||
Room,
|
||||
IndexedDBStore,
|
||||
RelationType,
|
||||
} from "../../src";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
|
||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
@ -1867,4 +1871,124 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
idbClient.stopClient();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@ -89,7 +89,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
room.addLiveEvents([thread.rootEvent]);
|
||||
await room.addLiveEvents([thread.rootEvent]);
|
||||
|
||||
// Initialize read receipt datastructure before testing the reaction
|
||||
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
|
||||
|
@ -42,6 +42,7 @@ import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
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]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
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]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
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]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
|
||||
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]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
|
||||
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]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom!.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
});
|
||||
|
||||
it("can be created with live events", () => {
|
||||
let seenLiveEvent = false;
|
||||
it("can be created with live events", async () => {
|
||||
const seenLiveEventDeferred = defer<boolean>();
|
||||
const listener = (
|
||||
ev: MatrixEvent,
|
||||
room?: Room,
|
||||
@ -371,43 +362,37 @@ describe("SlidingSyncSdk", () => {
|
||||
) => {
|
||||
if (timelineData?.liveEvent) {
|
||||
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
|
||||
seenLiveEvent = true;
|
||||
seenLiveEventDeferred.resolve(true);
|
||||
}
|
||||
};
|
||||
client!.on(RoomEvent.Timeline, listener);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
client!.off(RoomEvent.Timeline, listener);
|
||||
const gotRoom = client!.getRoom(roomH);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
// check the entire timeline is correct
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
expect(seenLiveEvent).toBe(true);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
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]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
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]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomF].name);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomF].name);
|
||||
});
|
||||
|
||||
describe("updating", () => {
|
||||
@ -419,33 +404,33 @@ describe("SlidingSyncSdk", () => {
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
const newTimeline = data[roomA].timeline;
|
||||
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 () => {
|
||||
let gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
@ -456,11 +441,11 @@ describe("SlidingSyncSdk", () => {
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
@ -471,11 +456,11 @@ describe("SlidingSyncSdk", () => {
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
@ -486,11 +471,11 @@ describe("SlidingSyncSdk", () => {
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
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
|
||||
@ -512,7 +497,7 @@ describe("SlidingSyncSdk", () => {
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
@ -530,7 +515,7 @@ describe("SlidingSyncSdk", () => {
|
||||
);
|
||||
|
||||
// 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 emitPromise(client!, RoomMemberEvent.Name);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
const inviteeMember = room.getMember(invitee)!;
|
||||
expect(inviteeMember).toBeDefined();
|
||||
expect(inviteeMember).toBeTruthy();
|
||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||
});
|
||||
@ -723,7 +708,7 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
});
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData).toBeTruthy();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
@ -744,6 +729,7 @@ describe("SlidingSyncSdk", () => {
|
||||
foo: "bar",
|
||||
};
|
||||
const roomType = "test";
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: [
|
||||
@ -755,9 +741,9 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeDefined();
|
||||
expect(event).toBeTruthy();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
@ -943,8 +929,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
@ -984,7 +971,7 @@ describe("SlidingSyncSdk", () => {
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
@ -1077,12 +1064,13 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
|
||||
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
|
||||
const receipt = room.getReadReceiptForUserId(alice);
|
||||
expect(receipt).toBeDefined();
|
||||
expect(receipt).toBeTruthy();
|
||||
expect(receipt?.eventId).toEqual(lastEvent.event_id);
|
||||
expect(receipt?.data.ts).toEqual(1234567);
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
|
@ -697,7 +697,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
|
||||
|
||||
// Ensure the root is in the room timeline
|
||||
root.setThreadId(root.getId());
|
||||
room.addLiveEvents([root]);
|
||||
await room.addLiveEvents([root]);
|
||||
|
||||
// Create the thread and wait for it to be initialised
|
||||
const thread = room.createThread(root.getId()!, root, [], false);
|
||||
|
@ -172,9 +172,9 @@ describe("Room", function () {
|
||||
* @param timestamp - Timestamp of the message
|
||||
* @return The message event
|
||||
*/
|
||||
const mkMessageInRoom = (room: Room, timestamp: number) => {
|
||||
const mkMessageInRoom = async (room: Room, timestamp: number) => {
|
||||
const message = mkMessage({ ts: timestamp });
|
||||
room.addLiveEvents([message]);
|
||||
await room.addLiveEvents([message]);
|
||||
return message;
|
||||
};
|
||||
|
||||
@ -319,23 +319,25 @@ describe("Room", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
expect(() => room.addLiveEvents(events, DuplicateStrategy.Replace, false)).not.toThrow();
|
||||
expect(() => room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).not.toThrow();
|
||||
it("Make sure legacy overload passing options directly as parameters still works", async () => {
|
||||
await expect(room.addLiveEvents(events, DuplicateStrategy.Replace, false)).resolves.not.toThrow();
|
||||
await expect(room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).resolves.not.toThrow();
|
||||
await expect(
|
||||
// @ts-ignore
|
||||
expect(() => room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false)).toThrow();
|
||||
room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function () {
|
||||
expect(function () {
|
||||
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", async function () {
|
||||
return expect(
|
||||
// @ts-ignore
|
||||
room.addLiveEvents(events, {
|
||||
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
|
||||
const dupe = utils.mkMessage({
|
||||
room: roomId,
|
||||
@ -344,15 +346,15 @@ describe("Room", function () {
|
||||
event: true,
|
||||
});
|
||||
dupe.event.event_id = events[0].getId();
|
||||
room.addLiveEvents(events);
|
||||
await room.addLiveEvents(events);
|
||||
expect(room.timeline[0]).toEqual(events[0]);
|
||||
room.addLiveEvents([dupe], {
|
||||
await room.addLiveEvents([dupe], {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
});
|
||||
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
|
||||
const dupe = utils.mkMessage({
|
||||
room: roomId,
|
||||
@ -361,16 +363,16 @@ describe("Room", function () {
|
||||
event: true,
|
||||
});
|
||||
dupe.event.event_id = events[0].getId();
|
||||
room.addLiveEvents(events);
|
||||
await room.addLiveEvents(events);
|
||||
expect(room.timeline[0]).toEqual(events[0]);
|
||||
// @ts-ignore
|
||||
room.addLiveEvents([dupe], {
|
||||
await room.addLiveEvents([dupe], {
|
||||
duplicateStrategy: "ignore",
|
||||
});
|
||||
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;
|
||||
room.on(RoomEvent.Timeline, function (event, emitRoom, toStart) {
|
||||
callCount += 1;
|
||||
@ -379,11 +381,11 @@ describe("Room", function () {
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(toStart).toBeFalsy();
|
||||
});
|
||||
room.addLiveEvents(events);
|
||||
await room.addLiveEvents(events);
|
||||
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[] = [
|
||||
utils.mkMembership({
|
||||
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[1]], { timelineWasEmpty: false });
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
@ -410,7 +412,7 @@ describe("Room", function () {
|
||||
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 = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
@ -422,11 +424,11 @@ describe("Room", function () {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
room.addLiveEvents(events);
|
||||
await room.addLiveEvents(events);
|
||||
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({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
@ -457,7 +459,7 @@ describe("Room", function () {
|
||||
expect(stub.mock.calls[0][3]).toBeUndefined();
|
||||
|
||||
// then the remoteEvent
|
||||
room.addLiveEvents([remoteEvent]);
|
||||
await room.addLiveEvents([remoteEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
expect(stub).toHaveBeenCalledTimes(2);
|
||||
@ -469,7 +471,7 @@ describe("Room", function () {
|
||||
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({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
@ -495,14 +497,14 @@ describe("Room", function () {
|
||||
// then /sync returns the remoteEvent, it should de-dupe based on the event ID.
|
||||
const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson));
|
||||
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
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
// but without the event ID matching we will still have the local event in pending events
|
||||
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({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
@ -525,7 +527,7 @@ describe("Room", function () {
|
||||
const realEventId = "$real-event-id";
|
||||
const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson));
|
||||
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
|
||||
|
||||
// then the /send request returns the real event ID.
|
||||
@ -538,7 +540,7 @@ describe("Room", function () {
|
||||
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({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
@ -547,7 +549,7 @@ describe("Room", function () {
|
||||
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
||||
|
||||
// add the remoteEvent
|
||||
room.addLiveEvents([remoteEvent]);
|
||||
await room.addLiveEvents([remoteEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
@ -612,7 +614,7 @@ describe("Room", 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 = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
@ -650,13 +652,13 @@ describe("Room", function () {
|
||||
event: true,
|
||||
content: { name: "Old Room Name" },
|
||||
});
|
||||
room.addLiveEvents([newEv]);
|
||||
await room.addLiveEvents([newEv]);
|
||||
expect(newEv.sender).toEqual(sentinel);
|
||||
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
|
||||
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 = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
@ -694,7 +696,7 @@ describe("Room", function () {
|
||||
skey: userA,
|
||||
event: true,
|
||||
});
|
||||
room.addLiveEvents([newEv]);
|
||||
await room.addLiveEvents([newEv]);
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
@ -763,12 +765,12 @@ describe("Room", function () {
|
||||
];
|
||||
});
|
||||
|
||||
it("should copy state from previous timeline", function () {
|
||||
room.addLiveEvents([events[0], events[1]]);
|
||||
it("should copy state from previous timeline", async function () {
|
||||
await room.addLiveEvents([events[0], events[1]]);
|
||||
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
|
||||
room.resetLiveTimeline("sometoken", "someothertoken");
|
||||
|
||||
room.addLiveEvents([events[2]]);
|
||||
await room.addLiveEvents([events[2]]);
|
||||
const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
|
||||
const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
expect(room.getLiveTimeline().getEvents().length).toEqual(1);
|
||||
@ -776,8 +778,8 @@ describe("Room", function () {
|
||||
expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]);
|
||||
});
|
||||
|
||||
it("should reset the legacy timeline fields", function () {
|
||||
room.addLiveEvents([events[0], events[1]]);
|
||||
it("should reset the legacy timeline fields", async function () {
|
||||
await room.addLiveEvents([events[0], events[1]]);
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
|
||||
const oldStateBeforeRunningReset = room.oldState;
|
||||
@ -798,7 +800,7 @@ describe("Room", function () {
|
||||
|
||||
room.resetLiveTimeline("sometoken", "someothertoken");
|
||||
|
||||
room.addLiveEvents([events[2]]);
|
||||
await room.addLiveEvents([events[2]]);
|
||||
const newLiveTimeline = room.getLiveTimeline();
|
||||
expect(room.timeline).toEqual(newLiveTimeline.getEvents());
|
||||
expect(room.oldState).toEqual(newLiveTimeline.getState(EventTimeline.BACKWARDS));
|
||||
@ -824,8 +826,8 @@ describe("Room", function () {
|
||||
expect(callCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", function () {
|
||||
room.addLiveEvents([events[0]]);
|
||||
it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", async function () {
|
||||
await room.addLiveEvents([events[0]]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
const firstLiveTimeline = room.getLiveTimeline();
|
||||
room.resetLiveTimeline("sometoken", "someothertoken");
|
||||
@ -868,8 +870,8 @@ describe("Room", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
it("should handle events in the same timeline", function () {
|
||||
room.addLiveEvents(events);
|
||||
it("should handle events in the same timeline", async function () {
|
||||
await room.addLiveEvents(events);
|
||||
|
||||
expect(
|
||||
room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!),
|
||||
@ -882,13 +884,13 @@ describe("Room", function () {
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should handle events in adjacent timelines", function () {
|
||||
it("should handle events in adjacent timelines", async function () {
|
||||
const oldTimeline = room.addTimeline();
|
||||
oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward);
|
||||
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward);
|
||||
|
||||
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()!),
|
||||
@ -898,11 +900,11 @@ describe("Room", function () {
|
||||
).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();
|
||||
|
||||
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(
|
||||
null,
|
||||
@ -912,8 +914,8 @@ describe("Room", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null for unknown events", function () {
|
||||
room.addLiveEvents(events);
|
||||
it("should return null for unknown events", async function () {
|
||||
await room.addLiveEvents(events);
|
||||
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, "xxx")).toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering("xxx", events[0].getId()!)).toBe(null);
|
||||
@ -990,8 +992,8 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
describe("recalculate", function () {
|
||||
const setJoinRule = function (rule: JoinRule) {
|
||||
room.addLiveEvents([
|
||||
const setJoinRule = async function (rule: JoinRule) {
|
||||
await room.addLiveEvents([
|
||||
utils.mkEvent({
|
||||
type: EventType.RoomJoinRules,
|
||||
room: roomId,
|
||||
@ -1003,8 +1005,8 @@ describe("Room", function () {
|
||||
}),
|
||||
]);
|
||||
};
|
||||
const setAltAliases = function (aliases: string[]) {
|
||||
room.addLiveEvents([
|
||||
const setAltAliases = async function (aliases: string[]) {
|
||||
await room.addLiveEvents([
|
||||
utils.mkEvent({
|
||||
type: EventType.RoomCanonicalAlias,
|
||||
room: roomId,
|
||||
@ -1016,8 +1018,8 @@ describe("Room", function () {
|
||||
}),
|
||||
]);
|
||||
};
|
||||
const setAlias = function (alias: string) {
|
||||
room.addLiveEvents([
|
||||
const setAlias = async function (alias: string) {
|
||||
await room.addLiveEvents([
|
||||
utils.mkEvent({
|
||||
type: EventType.RoomCanonicalAlias,
|
||||
room: roomId,
|
||||
@ -1027,8 +1029,8 @@ describe("Room", function () {
|
||||
}),
|
||||
]);
|
||||
};
|
||||
const setRoomName = function (name: string) {
|
||||
room.addLiveEvents([
|
||||
const setRoomName = async function (name: string) {
|
||||
await room.addLiveEvents([
|
||||
utils.mkEvent({
|
||||
type: EventType.RoomName,
|
||||
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.mship = state;
|
||||
opts.user = opts.user || userId;
|
||||
opts.skey = userId;
|
||||
opts.event = true;
|
||||
const event = utils.mkMembership(opts);
|
||||
room.addLiveEvents([event]);
|
||||
await room.addLiveEvents([event]);
|
||||
return event;
|
||||
};
|
||||
|
||||
@ -1059,10 +1061,10 @@ describe("Room", function () {
|
||||
describe("Room.recalculate => Stripped State Events", function () {
|
||||
it(
|
||||
"should set stripped state events as actual state events if the " + "room is an invite room",
|
||||
function () {
|
||||
async function () {
|
||||
const roomName = "flibble";
|
||||
|
||||
const event = addMember(userA, "invite");
|
||||
const event = await addMember(userA, "invite");
|
||||
event.event.unsigned = {};
|
||||
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 () {
|
||||
const event = addMember(userA, "join");
|
||||
it("should not clobber state events if it isn't an invite room", async function () {
|
||||
const event = await addMember(userA, "join");
|
||||
const roomName = "flibble";
|
||||
setRoomName(roomName);
|
||||
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[] = [
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
@ -1559,7 +1561,7 @@ describe("Room", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
room.addLiveEvents(events);
|
||||
await room.addLiveEvents(events);
|
||||
const ts = 13787898424;
|
||||
|
||||
// check it initialises correctly
|
||||
@ -1575,7 +1577,7 @@ describe("Room", function () {
|
||||
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[] = [
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
@ -1597,7 +1599,7 @@ describe("Room", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
room.addLiveEvents(events);
|
||||
await room.addLiveEvents(events);
|
||||
const ts = 13787898424;
|
||||
|
||||
// check it initialises correctly
|
||||
@ -1673,7 +1675,9 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
describe("addPendingEvent", function () {
|
||||
it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function () {
|
||||
it(
|
||||
"should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'",
|
||||
async function () {
|
||||
const client = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
client.supportsThreads = () => true;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
@ -1698,14 +1702,17 @@ describe("Room", function () {
|
||||
msg: "remote 2",
|
||||
event: true,
|
||||
});
|
||||
room.addLiveEvents([eventA]);
|
||||
await room.addLiveEvents([eventA]);
|
||||
room.addPendingEvent(eventB, "TXN1");
|
||||
room.addLiveEvents([eventC]);
|
||||
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(
|
||||
"should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'",
|
||||
async function () {
|
||||
const room = new Room(roomId, new TestClient(userA).client, userA, {
|
||||
pendingEventOrdering: PendingEventOrdering.Chronological,
|
||||
});
|
||||
@ -1728,11 +1735,12 @@ describe("Room", function () {
|
||||
msg: "remote 2",
|
||||
event: true,
|
||||
});
|
||||
room.addLiveEvents([eventA]);
|
||||
await room.addLiveEvents([eventA]);
|
||||
room.addPendingEvent(eventB, "TXN1");
|
||||
room.addLiveEvents([eventC]);
|
||||
await room.addLiveEvents([eventC]);
|
||||
expect(room.timeline).toEqual([eventA, eventB, eventC]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should apply redactions eagerly in the pending event list", () => {
|
||||
const client = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
@ -2004,9 +2012,9 @@ describe("Room", function () {
|
||||
});
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userB,
|
||||
mship: "join",
|
||||
@ -2070,9 +2078,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2091,9 +2099,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2112,9 +2120,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2133,9 +2141,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2154,9 +2162,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2182,9 +2190,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2219,9 +2227,9 @@ describe("Room", 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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2249,9 +2257,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2279,9 +2287,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2309,9 +2317,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2339,9 +2347,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2377,9 +2385,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2415,9 +2423,9 @@ describe("Room", function () {
|
||||
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);
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
@ -2548,7 +2556,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
||||
await room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
||||
const thread: Thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
@ -2575,7 +2583,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
room.addLiveEvents([threadResponseEdit]);
|
||||
await room.addLiveEvents([threadResponseEdit]);
|
||||
await prom;
|
||||
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);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
@ -2641,7 +2649,7 @@ describe("Room", function () {
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await room.addLiveEvents([threadResponse1Redaction]);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(1);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
@ -2674,7 +2682,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
@ -2682,7 +2690,7 @@ describe("Room", function () {
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
|
||||
room.addLiveEvents([threadResponse2ReactionRedaction]);
|
||||
await room.addLiveEvents([threadResponse2ReactionRedaction]);
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
});
|
||||
@ -2714,7 +2722,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
@ -2723,7 +2731,7 @@ describe("Room", function () {
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadRootRedaction = mkRedaction(threadRoot);
|
||||
room.addLiveEvents([threadRootRedaction]);
|
||||
await room.addLiveEvents([threadRootRedaction]);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(2);
|
||||
});
|
||||
@ -2776,12 +2784,12 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1]);
|
||||
await room.addLiveEvents([threadRoot, threadResponse1]);
|
||||
const thread: Thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread.initialEventsFetched).toBeTruthy();
|
||||
room.addLiveEvents([threadResponse2]);
|
||||
await room.addLiveEvents([threadResponse2]);
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
@ -2802,7 +2810,7 @@ describe("Room", function () {
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
||||
room.addLiveEvents([threadResponse2Redaction]);
|
||||
await room.addLiveEvents([threadResponse2Redaction]);
|
||||
await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
expect(thread).toHaveLength(1);
|
||||
@ -2826,7 +2834,7 @@ describe("Room", function () {
|
||||
prom = emitPromise(room, ThreadEvent.Delete);
|
||||
const prom2 = emitPromise(room, RoomEvent.Timeline);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await room.addLiveEvents([threadResponse1Redaction]);
|
||||
await prom;
|
||||
await prom2;
|
||||
expect(thread).toHaveLength(0);
|
||||
@ -2946,7 +2954,7 @@ describe("Room", function () {
|
||||
const events = [threadRoot, rootReaction, threadResponse, threadReaction];
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents(events);
|
||||
await room.addLiveEvents(events);
|
||||
const thread = await prom;
|
||||
expect(thread).toBe(threadRoot.getThread());
|
||||
expect(thread.rootEvent).toBe(threadRoot);
|
||||
@ -3452,21 +3460,21 @@ describe("Room", function () {
|
||||
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");
|
||||
room.addLiveEvents([roomCreateEvent("roomid", null)]);
|
||||
await room.addLiveEvents([roomCreateEvent("roomid", null)]);
|
||||
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");
|
||||
room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]);
|
||||
await room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]);
|
||||
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");
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
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");
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
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");
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid"),
|
||||
]);
|
||||
@ -3503,9 +3511,9 @@ describe("Room", function () {
|
||||
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");
|
||||
room.addLiveEvents([
|
||||
await room.addLiveEvents([
|
||||
roomCreateEvent("roomid", null), // Create event has no predecessor
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"),
|
||||
]);
|
||||
@ -3520,8 +3528,8 @@ describe("Room", function () {
|
||||
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", () => {
|
||||
const lastEventInMainTimeline = mkMessageInRoom(room, 23);
|
||||
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 = await mkMessageInRoom(room, 23);
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
|
||||
});
|
||||
|
||||
@ -3536,29 +3544,29 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
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", () => {
|
||||
mkMessageInRoom(room, 23);
|
||||
it("and the last event is in a thread, it should return the last event from the thread", async () => {
|
||||
await mkMessageInRoom(room, 23);
|
||||
const { thread } = mkThread({ room, length: 0 });
|
||||
const lastEventInThread = mkMessageInThread(thread, 42);
|
||||
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", () => {
|
||||
const lastEventInMainTimeline = mkMessageInRoom(room, 42);
|
||||
it("and the last event is in the main timeline, it should return the last event from the main timeline", async () => {
|
||||
const lastEventInMainTimeline = await mkMessageInRoom(room, 42);
|
||||
const { thread } = mkThread({ room, length: 0 });
|
||||
mkMessageInThread(thread, 23);
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
|
||||
});
|
||||
|
||||
it("and both events have the same timestamp, it should return the last event from the thread", () => {
|
||||
mkMessageInRoom(room, 23);
|
||||
it("and both events have the same timestamp, it should return the last event from the thread", async () => {
|
||||
await mkMessageInRoom(room, 23);
|
||||
const { thread } = mkThread({ room, length: 0 });
|
||||
const lastEventInThread = mkMessageInThread(thread, 23);
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInThread);
|
||||
});
|
||||
|
||||
it("and there is a thread without any messages, it should return the last event from the main timeline", () => {
|
||||
const lastEventInMainTimeline = mkMessageInRoom(room, 23);
|
||||
it("and there is a thread without any messages, it should return the last event from the main timeline", async () => {
|
||||
const lastEventInMainTimeline = await mkMessageInRoom(room, 23);
|
||||
mkThread({ room, length: 0 });
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
|
||||
});
|
||||
|
@ -235,6 +235,13 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
|
||||
"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 {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
@ -5573,11 +5573,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
room.currentState.setUnknownStateEvents(stateEvents);
|
||||
}
|
||||
|
||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
const [timelineEvents, threadedEvents, unknownRelations] =
|
||||
room.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
this.processThreadEvents(room, threadedEvents, true);
|
||||
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
|
||||
|
||||
room.oldState.paginationToken = res.end ?? null;
|
||||
if (res.chunk.length === 0) {
|
||||
@ -5686,11 +5688,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
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);
|
||||
// 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.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
|
||||
// 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 timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
this.processThreadRoots(
|
||||
@ -6238,6 +6241,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)),
|
||||
false,
|
||||
);
|
||||
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
|
||||
|
||||
const atEnd = res.end === undefined || res.end === res.start;
|
||||
|
||||
|
@ -24,7 +24,13 @@ import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
|
||||
import type { IEventDecryptionResult } from "../@types/crypto";
|
||||
import { logger } from "../logger";
|
||||
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 { deepSortedObjectEntries, internaliseString } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
@ -63,6 +69,7 @@ export interface IUnsigned {
|
||||
"transaction_id"?: string;
|
||||
"invite_room_state"?: StrippedState[];
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
[UNSIGNED_THREAD_ID_FIELD.name]?: string;
|
||||
}
|
||||
|
||||
export interface IThreadBundledRelationship {
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
|
||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||
RelationType,
|
||||
UNSIGNED_THREAD_ID_FIELD,
|
||||
} from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||
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);
|
||||
}
|
||||
|
||||
if (!event.isRelation()) {
|
||||
return {
|
||||
shouldLiveInRoom: true,
|
||||
shouldLiveInThread: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Edge case where we know the event is a relation but don't have the parentEvent
|
||||
if (roots?.has(event.relationEventId!)) {
|
||||
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: true,
|
||||
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 {
|
||||
shouldLiveInRoom: false,
|
||||
shouldLiveInThread: false,
|
||||
};
|
||||
}
|
||||
@ -2156,14 +2175,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
|
||||
let thread = this.getThread(threadId);
|
||||
|
||||
if (!thread) {
|
||||
const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId);
|
||||
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
|
||||
}
|
||||
|
||||
const thread = this.getThread(threadId);
|
||||
if (thread) {
|
||||
thread.addEvents(events, toStartOfTimeline);
|
||||
} else {
|
||||
const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId);
|
||||
this.createThread(threadId, rootEvent, events, toStartOfTimeline);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2700,16 +2718,20 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @param addLiveEventOptions - addLiveEvent options
|
||||
* @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`
|
||||
*/
|
||||
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache?: boolean): void;
|
||||
public addLiveEvents(
|
||||
public async addLiveEvents(
|
||||
events: MatrixEvent[],
|
||||
duplicateStrategy?: DuplicateStrategy,
|
||||
fromCache?: boolean,
|
||||
): Promise<void>;
|
||||
public async addLiveEvents(
|
||||
events: MatrixEvent[],
|
||||
duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
|
||||
fromCache = false,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy;
|
||||
let timelineWasEmpty: boolean | undefined = false;
|
||||
if (typeof duplicateStrategyOrOpts === "object") {
|
||||
@ -2760,6 +2782,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
timelineWasEmpty,
|
||||
};
|
||||
|
||||
// List of extra events to check for being parents of any relations encountered
|
||||
const neighbouringEvents = [...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".
|
||||
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,
|
||||
events,
|
||||
neighbouringEvents,
|
||||
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 ?? ""]) {
|
||||
eventsByThread[threadId ?? ""] = [];
|
||||
}
|
||||
@ -2786,6 +2834,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
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(
|
||||
events: MatrixEvent[],
|
||||
): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[]] {
|
||||
): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[], unknownRelations: MatrixEvent[]] {
|
||||
// Indices to the events array, for readability
|
||||
const ROOM = 0;
|
||||
const THREAD = 1;
|
||||
const UNKNOWN_RELATION = 2;
|
||||
if (this.client.supportsThreads()) {
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
return events.reduce(
|
||||
return events.reduce<[MatrixEvent[], MatrixEvent[], MatrixEvent[]]>(
|
||||
(memo, event: MatrixEvent) => {
|
||||
const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
|
||||
event,
|
||||
@ -2819,13 +2870,17 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
memo[THREAD].push(event);
|
||||
}
|
||||
|
||||
if (!shouldLiveInThread && !shouldLiveInRoom) {
|
||||
memo[UNKNOWN_RELATION].push(event);
|
||||
}
|
||||
|
||||
return memo;
|
||||
},
|
||||
[[] as MatrixEvent[], [] as MatrixEvent[]],
|
||||
[[], [], []],
|
||||
);
|
||||
} else {
|
||||
// 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)) {
|
||||
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;
|
||||
}
|
||||
|
@ -628,7 +628,7 @@ export class SlidingSyncSdk {
|
||||
|
||||
if (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) {
|
||||
room.recalculate();
|
||||
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
|
||||
room.addEphemeralEvents(ephemeralEvents);
|
||||
@ -747,12 +747,12 @@ export class SlidingSyncSdk {
|
||||
* @param numLive - the number of events in timelineEventList which just happened,
|
||||
* supplied from the server.
|
||||
*/
|
||||
public injectRoomEvents(
|
||||
public async injectRoomEvents(
|
||||
room: Room,
|
||||
stateEventList: MatrixEvent[],
|
||||
timelineEventList?: MatrixEvent[],
|
||||
numLive?: number,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
timelineEventList = timelineEventList || [];
|
||||
stateEventList = stateEventList || [];
|
||||
numLive = numLive || 0;
|
||||
@ -811,11 +811,11 @@ export class SlidingSyncSdk {
|
||||
// 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
|
||||
// to be decorated with sender etc.
|
||||
room.addLiveEvents(timelineEventList, {
|
||||
await room.addLiveEvents(timelineEventList, {
|
||||
fromCache: true,
|
||||
});
|
||||
if (liveTimelineEvents.length > 0) {
|
||||
room.addLiveEvents(liveTimelineEvents, {
|
||||
await room.addLiveEvents(liveTimelineEvents, {
|
||||
fromCache: false,
|
||||
});
|
||||
}
|
||||
|
21
src/sync.ts
21
src/sync.ts
@ -501,7 +501,7 @@ export class SyncApi {
|
||||
},
|
||||
)
|
||||
.then(
|
||||
(res) => {
|
||||
async (res) => {
|
||||
if (this._peekRoom !== peekRoom) {
|
||||
debuglog("Stopped peeking in room %s", peekRoom.roomId);
|
||||
return;
|
||||
@ -541,7 +541,7 @@ export class SyncApi {
|
||||
})
|
||||
.map(this.client.getEventMapper());
|
||||
|
||||
peekRoom.addLiveEvents(events);
|
||||
await peekRoom.addLiveEvents(events);
|
||||
this.peekPoll(peekRoom, res.end);
|
||||
},
|
||||
(err) => {
|
||||
@ -899,8 +899,6 @@ export class SyncApi {
|
||||
// Reset after a successful sync
|
||||
this.failedSyncCount = 0;
|
||||
|
||||
await this.client.store.setSyncData(data);
|
||||
|
||||
const syncEventData = {
|
||||
oldSyncToken: syncToken ?? undefined,
|
||||
nextSyncToken: data.next_batch,
|
||||
@ -924,6 +922,10 @@ export class SyncApi {
|
||||
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
|
||||
syncEventData.catchingUp = this.catchingUp;
|
||||
|
||||
@ -1627,16 +1629,17 @@ export class SyncApi {
|
||||
return Object.keys(obj)
|
||||
.filter((k) => !unsafeProp(k))
|
||||
.map((roomId) => {
|
||||
const arrObj = obj[roomId] as T & { room: Room; isBrandNewRoom: boolean };
|
||||
let room = client.store.getRoom(roomId);
|
||||
let isBrandNewRoom = false;
|
||||
if (!room) {
|
||||
room = this.createRoom(roomId);
|
||||
isBrandNewRoom = true;
|
||||
}
|
||||
arrObj.room = room;
|
||||
arrObj.isBrandNewRoom = isBrandNewRoom;
|
||||
return arrObj;
|
||||
return {
|
||||
...obj[roomId],
|
||||
room,
|
||||
isBrandNewRoom,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -1773,7 +1776,7 @@ export class SyncApi {
|
||||
// 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
|
||||
// to be decorated with sender etc.
|
||||
room.addLiveEvents(timelineEventList || [], {
|
||||
await room.addLiveEvents(timelineEventList || [], {
|
||||
fromCache,
|
||||
timelineWasEmpty,
|
||||
});
|
||||
|
Reference in New Issue
Block a user