mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-06-08 15:21:53 +03:00
* Switch sliding sync support to simplified sliding sync Experimental PR to test js-sdk with simlified sliding sync. This does not maintain support for regulaer sliding sync. * Remove txn_id handling, ensure we always resend when req params change * Fix some tests * Fix remaining tests * Mark TODOs on tests which need to die * Linting * Make comments lie less * void * Always sent full extension request * Fix test * Remove usage of deprecated field * Hopefully fix DM names * Refactor how heroes are handled in Room * Fix how heroes work * Linting * Ensure that when SSS omits heroes we don't forget we had heroes Otherwise when the room next appears the name/avatar reset to 'Empty Room' with no avatar. * Check the right flag when doing timeline trickling * Also change when the backpagination token is set * Remove list ops and server-provided sort positions SSS doesn't have them. * Linting * Add Room.bumpStamp * Update crypto wasm lib For new functions * Add performance logging * Fix breaking change in crypto wasm v8 * Update crypto wasm for breaking changes See https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/releases/tag/v8.0.0 for how this was mapped from the previous API. * Mark all tracked users as dirty on expired SSS connections See https://github.com/matrix-org/matrix-rust-sdk/pull/3965 for more information. Requires `Extension.onRequest` to be `async`. * add ts extension * Fix typedoc ref * Add method to interface * Don't force membership to invite The membership was set correctly from the stripped state anyway so this was redundant and was breaking rooms where we'd knocked. * Missed merge * Type import * Make coverage happier * More test coverage * Grammar & formatting Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Remove markAllTrackedUsersAsDirty from crypto API Not sure why this was in there, seems like it just needed to be in crypto sync callbacks, which it already was. * Remove I from interface * API doc * Move Hero definition to room-summary * make comment more specific * Move internal details into room.ts and make the comment a proper tsdoc comment * Use terser arrow function syntax Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Move comment to where we do the lookup * Clarify comment also prettier says hi * Add comment Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Add tsdoc explaining that the summary event will be modified * more comment * Remove unrelated changes * Add docs & make fields optional * Type import * Clarify sync versions Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Make tsdoc comment & add info on when it's used. Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Rephrase comment Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Prettier * Only fetch member for hero in legacy sync mode * Split out a separate method to set SSS room summary Rather than trying to fudge up an object that looked enough like the old one that we could pass it in. * Type import * Make link work * Nope, linter treats it as an unused import * Add link the other way * Add more detail to doc Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Remove unnecessary cast Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Remove length > 0 check as it wasn't really necessary and may cause heroes not to be cleared? * Doc params * Remove unnecessary undefined comparison Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Put the comparison back as it's necessary to stop typescript complaining * Fix comment * Fix comment --------- Co-authored-by: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
4295 lines
181 KiB
TypeScript
4295 lines
181 KiB
TypeScript
/*
|
|
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* This is an internal module. See {@link MatrixClient} for the public class.
|
|
*/
|
|
|
|
import { mocked } from "jest-mock";
|
|
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, type Optional, PollStartEvent } from "matrix-events-sdk";
|
|
|
|
import * as utils from "../test-utils/test-utils";
|
|
import { emitPromise, type IMessageOpts } from "../test-utils/test-utils";
|
|
import {
|
|
Direction,
|
|
DuplicateStrategy,
|
|
EventStatus,
|
|
type EventTimelineSet,
|
|
EventType,
|
|
Filter,
|
|
FILTER_RELATED_BY_REL_TYPES,
|
|
FILTER_RELATED_BY_SENDERS,
|
|
type IContent,
|
|
type IEvent,
|
|
type IRelationsRequestOpts,
|
|
type IStateEventWithRoomId,
|
|
JoinRule,
|
|
type MatrixClient,
|
|
MatrixEvent,
|
|
MatrixEventEvent,
|
|
PendingEventOrdering,
|
|
PollEvent,
|
|
RelationType,
|
|
RoomEvent,
|
|
type RoomMember,
|
|
} from "../../src";
|
|
import { EventTimeline } from "../../src/models/event-timeline";
|
|
import { NotificationCountType, Room } from "../../src/models/room";
|
|
import { RoomState } from "../../src/models/room-state";
|
|
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
|
import { TestClient } from "../TestClient";
|
|
import { ReceiptType, type WrappedReceipt } from "../../src/@types/read_receipts";
|
|
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
|
import * as threadUtils from "../test-utils/thread";
|
|
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
|
|
import { logger } from "../../src/logger";
|
|
import { flushPromises } from "../test-utils/flushPromises";
|
|
import { KnownMembership } from "../../src/@types/membership";
|
|
import type { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
|
|
|
describe("Room", function () {
|
|
const roomId = "!foo:bar";
|
|
const userA = "@alice:bar";
|
|
const userB = "@bertha:bar";
|
|
const userC = "@clarissa:bar";
|
|
const userD = "@dorothy:bar";
|
|
let room: Room;
|
|
|
|
const mkMessage = (opts?: Partial<IMessageOpts>) =>
|
|
utils.mkMessage(
|
|
{
|
|
...opts,
|
|
event: true,
|
|
user: userA,
|
|
room: roomId,
|
|
},
|
|
room.client,
|
|
);
|
|
|
|
const mkReply = (target: MatrixEvent) =>
|
|
utils.mkEvent(
|
|
{
|
|
event: true,
|
|
type: EventType.RoomMessage,
|
|
user: userA,
|
|
room: roomId,
|
|
content: {
|
|
"body": "Reply :: " + Math.random(),
|
|
"m.relates_to": {
|
|
"m.in_reply_to": {
|
|
event_id: target.getId()!,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
room.client,
|
|
);
|
|
|
|
const mkEdit = (target: MatrixEvent, salt = Math.random()) =>
|
|
utils.mkEvent(
|
|
{
|
|
event: true,
|
|
type: EventType.RoomMessage,
|
|
user: userA,
|
|
room: roomId,
|
|
content: {
|
|
"body": "* Edit of :: " + target.getId() + " :: " + salt,
|
|
"m.new_content": {
|
|
body: "Edit of :: " + target.getId() + " :: " + salt,
|
|
},
|
|
"m.relates_to": {
|
|
rel_type: RelationType.Replace,
|
|
event_id: target.getId()!,
|
|
},
|
|
},
|
|
},
|
|
room.client,
|
|
);
|
|
|
|
const mkThreadResponse = (root: MatrixEvent, opts?: Partial<IMessageOpts>) =>
|
|
utils.mkEvent(
|
|
{
|
|
...opts,
|
|
event: true,
|
|
type: EventType.RoomMessage,
|
|
user: userA,
|
|
room: roomId,
|
|
content: {
|
|
"body": "Thread response :: " + Math.random(),
|
|
"m.relates_to": {
|
|
"event_id": root.getId()!,
|
|
"m.in_reply_to": {
|
|
event_id: root.getId()!,
|
|
},
|
|
"rel_type": "m.thread",
|
|
},
|
|
},
|
|
},
|
|
room.client,
|
|
);
|
|
|
|
const mkRedaction = (target: MatrixEvent) =>
|
|
utils.mkEvent(
|
|
{
|
|
event: true,
|
|
type: EventType.RoomRedaction,
|
|
user: userA,
|
|
room: roomId,
|
|
redacts: target.getId()!,
|
|
content: {},
|
|
},
|
|
room.client,
|
|
);
|
|
|
|
/**
|
|
* @see threadUtils.mkThread
|
|
*/
|
|
const mkThread = (
|
|
opts: Partial<Parameters<typeof threadUtils.mkThread>[0]>,
|
|
): ReturnType<typeof threadUtils.mkThread> => {
|
|
return threadUtils.mkThread({
|
|
room,
|
|
client: new TestClient().client,
|
|
authorId: "@bob:example.org",
|
|
participantUserIds: ["@bob:example.org"],
|
|
...opts,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a message and adds it to the end of the main live timeline.
|
|
*
|
|
* @param room - Room to add the message to
|
|
* @param timestamp - Timestamp of the message
|
|
* @return The message event
|
|
*/
|
|
const mkMessageInRoom = async (room: Room, timestamp: number) => {
|
|
const message = mkMessage({ ts: timestamp });
|
|
await room.addLiveEvents([message], { addToState: false });
|
|
return message;
|
|
};
|
|
|
|
/**
|
|
* Creates a message in a thread and adds it to the end of the thread live timeline.
|
|
*
|
|
* @param thread - Thread to add the message to
|
|
* @param timestamp - Timestamp of the message
|
|
* @returns The thread message event
|
|
*/
|
|
const mkMessageInThread = (thread: Thread, timestamp: number) => {
|
|
const message = mkThreadResponse(thread.rootEvent!, { ts: timestamp });
|
|
thread.liveTimeline.addEvent(message, { toStartOfTimeline: false, addToState: false });
|
|
return message;
|
|
};
|
|
|
|
const addRoomThreads = (
|
|
room: Room,
|
|
thread1EventTs: Optional<number>,
|
|
thread2EventTs: Optional<number>,
|
|
): { thread1?: Thread; thread2?: Thread } => {
|
|
const result: { thread1?: Thread; thread2?: Thread } = {};
|
|
|
|
if (thread1EventTs !== null) {
|
|
const { rootEvent: thread1RootEvent, thread: thread1 } = mkThread({ room });
|
|
const thread1Event = mkThreadResponse(thread1RootEvent, { ts: thread1EventTs });
|
|
thread1.liveTimeline.addEvent(thread1Event, { toStartOfTimeline: true, addToState: false });
|
|
result.thread1 = thread1;
|
|
}
|
|
|
|
if (thread2EventTs !== null) {
|
|
const { rootEvent: thread2RootEvent, thread: thread2 } = mkThread({ room });
|
|
const thread2Event = mkThreadResponse(thread2RootEvent, { ts: thread2EventTs });
|
|
thread2.liveTimeline.addEvent(thread2Event, { toStartOfTimeline: true, addToState: false });
|
|
result.thread2 = thread2;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
beforeEach(function () {
|
|
room = new Room(roomId, new TestClient(userA, "device").client, userA);
|
|
// mock RoomStates
|
|
// @ts-ignore
|
|
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
|
|
// @ts-ignore
|
|
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
|
|
|
|
jest.spyOn(logger, "warn");
|
|
});
|
|
|
|
describe("getCreator", () => {
|
|
it("should return the sender from m.room.create", function () {
|
|
// @ts-ignore - mocked doesn't handle overloads sanely
|
|
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
|
if (type === EventType.RoomCreate && key === "") {
|
|
return utils.mkEvent({
|
|
event: true,
|
|
type: EventType.RoomCreate,
|
|
skey: "",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
creator: userB, // The creator field was dropped in room version 11 but a malicious client might still send it
|
|
},
|
|
});
|
|
}
|
|
});
|
|
const roomCreator = room.getCreator();
|
|
expect(roomCreator).toStrictEqual(userA);
|
|
});
|
|
|
|
it("should return null if the sender is undefined", function () {
|
|
// @ts-ignore - mocked doesn't handle overloads sanely
|
|
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
|
if (type === EventType.RoomCreate && key === "") {
|
|
return utils.mkEvent({
|
|
event: true,
|
|
type: EventType.RoomCreate,
|
|
skey: "",
|
|
room: roomId,
|
|
user: undefined,
|
|
content: {},
|
|
});
|
|
}
|
|
});
|
|
const roomCreator = room.getCreator();
|
|
expect(roomCreator).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getAvatarUrl", function () {
|
|
const hsUrl = "https://my.home.server";
|
|
|
|
it("should return the URL from m.room.avatar preferentially", function () {
|
|
// @ts-ignore - mocked doesn't handle overloads sanely
|
|
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
|
if (type === EventType.RoomAvatar && key === "") {
|
|
return utils.mkEvent({
|
|
event: true,
|
|
type: EventType.RoomAvatar,
|
|
skey: "",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
url: "mxc://flibble/wibble",
|
|
},
|
|
});
|
|
}
|
|
});
|
|
const url = room.getAvatarUrl(hsUrl, 100, 100, "scale");
|
|
// we don't care about how the mxc->http conversion is done, other
|
|
// than it contains the mxc body.
|
|
expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
|
|
});
|
|
|
|
it("should return nothing if there is no m.room.avatar and allowDefault=false", function () {
|
|
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
|
expect(url).toEqual(null);
|
|
});
|
|
|
|
it("should return unauthenticated media URL if useAuthentication is not set", function () {
|
|
// @ts-ignore - mocked doesn't handle overloads sanely
|
|
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
|
if (type === EventType.RoomAvatar && key === "") {
|
|
return utils.mkEvent({
|
|
event: true,
|
|
type: EventType.RoomAvatar,
|
|
skey: "",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
url: "mxc://flibble/wibble",
|
|
},
|
|
});
|
|
}
|
|
});
|
|
const url = room.getAvatarUrl(hsUrl, 100, 100, "scale");
|
|
// Check for unauthenticated media prefix
|
|
expect(url?.indexOf("/_matrix/media/v3/")).not.toEqual(-1);
|
|
});
|
|
|
|
it("should return authenticated media URL if useAuthentication=true", function () {
|
|
// @ts-ignore - mocked doesn't handle overloads sanely
|
|
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
|
if (type === EventType.RoomAvatar && key === "") {
|
|
return utils.mkEvent({
|
|
event: true,
|
|
type: EventType.RoomAvatar,
|
|
skey: "",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
url: "mxc://flibble/wibble",
|
|
},
|
|
});
|
|
}
|
|
});
|
|
const url = room.getAvatarUrl(hsUrl, 100, 100, "scale", undefined, true);
|
|
// Check for authenticated media prefix
|
|
expect(url?.indexOf("/_matrix/client/v1/media/")).not.toEqual(-1);
|
|
});
|
|
});
|
|
|
|
describe("getMember", function () {
|
|
beforeEach(function () {
|
|
mocked(room.currentState.getMember).mockImplementation(function (userId) {
|
|
return (
|
|
{
|
|
"@alice:bar": {
|
|
userId: userA,
|
|
roomId: roomId,
|
|
} as unknown as RoomMember,
|
|
}[userId] || null
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should return null if the member isn't in current state", function () {
|
|
expect(room.getMember("@bar:foo")).toEqual(null);
|
|
});
|
|
|
|
it("should return the member from current state", function () {
|
|
expect(room.getMember(userA)).not.toEqual(null);
|
|
});
|
|
});
|
|
|
|
describe("addLiveEvents", function () {
|
|
const events: MatrixEvent[] = [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "changing room name",
|
|
event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: { name: "New Room Name" },
|
|
}),
|
|
];
|
|
|
|
it("should replace a timeline event if dupe strategy is 'replace'", async function () {
|
|
// make a duplicate
|
|
const dupe = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "dupe",
|
|
event: true,
|
|
});
|
|
dupe.event.event_id = events[0].getId();
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
expect(room.timeline[0]).toEqual(events[0]);
|
|
await room.addLiveEvents([dupe], {
|
|
duplicateStrategy: DuplicateStrategy.Replace,
|
|
addToState: false,
|
|
});
|
|
expect(room.timeline[0]).toEqual(dupe);
|
|
});
|
|
|
|
it("should ignore a given dupe event if dupe strategy is 'ignore'", async function () {
|
|
// make a duplicate
|
|
const dupe = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "dupe",
|
|
event: true,
|
|
});
|
|
dupe.event.event_id = events[0].getId();
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
expect(room.timeline[0]).toEqual(events[0]);
|
|
// @ts-ignore
|
|
await room.addLiveEvents([dupe], {
|
|
duplicateStrategy: DuplicateStrategy.Ignore,
|
|
});
|
|
expect(room.timeline[0]).toEqual(events[0]);
|
|
});
|
|
|
|
it("should emit 'Room.timeline' events", async function () {
|
|
let callCount = 0;
|
|
room.on(RoomEvent.Timeline, function (event, emitRoom, toStart) {
|
|
callCount += 1;
|
|
expect(room.timeline.length).toEqual(callCount);
|
|
expect(event).toEqual(events[callCount - 1]);
|
|
expect(emitRoom).toEqual(room);
|
|
expect(toStart).toBeFalsy();
|
|
});
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
expect(callCount).toEqual(2);
|
|
});
|
|
|
|
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,
|
|
mship: KnownMembership.Invite,
|
|
user: userB,
|
|
skey: userA,
|
|
event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userB,
|
|
event: true,
|
|
content: {
|
|
name: "New room",
|
|
},
|
|
}),
|
|
];
|
|
await room.addLiveEvents(events, { addToState: true });
|
|
expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: false });
|
|
expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: false });
|
|
expect(events[0].forwardLooking).toBe(true);
|
|
expect(events[1].forwardLooking).toBe(true);
|
|
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should synthesize read receipts for the senders of events", async function () {
|
|
const sentinel = {
|
|
userId: userA,
|
|
membership: KnownMembership.Join,
|
|
name: "Alice",
|
|
} as unknown as RoomMember;
|
|
mocked(room.currentState.getSentinelMember).mockImplementation(function (uid) {
|
|
if (uid === userA) {
|
|
return sentinel;
|
|
}
|
|
return null;
|
|
});
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
|
|
});
|
|
|
|
it("should emit Room.localEchoUpdated when a local echo is updated", async function () {
|
|
const localEvent = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
});
|
|
localEvent.status = EventStatus.SENDING;
|
|
const localEventId = localEvent.getId();
|
|
|
|
const remoteEvent = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
});
|
|
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
|
const remoteEventId = remoteEvent.getId();
|
|
|
|
const stub = jest.fn();
|
|
room.on(RoomEvent.LocalEchoUpdated, stub);
|
|
|
|
// first add the local echo
|
|
room.addPendingEvent(localEvent, "TXN_ID");
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
expect(stub.mock.calls[0][0].getId()).toEqual(localEventId);
|
|
expect(stub.mock.calls[0][0].status).toEqual(EventStatus.SENDING);
|
|
expect(stub.mock.calls[0][1]).toEqual(room);
|
|
expect(stub.mock.calls[0][2]).toBeUndefined();
|
|
expect(stub.mock.calls[0][3]).toBeUndefined();
|
|
|
|
// then the remoteEvent
|
|
await room.addLiveEvents([remoteEvent], { addToState: false });
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
expect(stub).toHaveBeenCalledTimes(2);
|
|
|
|
expect(stub.mock.calls[1][0].getId()).toEqual(remoteEventId);
|
|
expect(stub.mock.calls[1][0].status).toBeNull();
|
|
expect(stub.mock.calls[1][1]).toEqual(room);
|
|
expect(stub.mock.calls[1][2]).toEqual(localEventId);
|
|
expect(stub.mock.calls[1][3]).toBe(EventStatus.SENDING);
|
|
});
|
|
|
|
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,
|
|
event: false,
|
|
});
|
|
delete eventJson["txn_id"];
|
|
delete eventJson["event_id"];
|
|
const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson));
|
|
localEvent.status = EventStatus.SENDING;
|
|
expect(localEvent.getTxnId()).toBeUndefined();
|
|
expect(room.timeline.length).toEqual(0);
|
|
|
|
// first add the local echo. This is done before the /send request is even sent.
|
|
const txnId = "My_txn_id";
|
|
room.addPendingEvent(localEvent, txnId);
|
|
expect(room.getEventForTxnId(txnId)).toEqual(localEvent);
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
// now the /send request returns the true event ID.
|
|
const realEventId = "$real-event-id";
|
|
room.updatePendingEvent(localEvent, EventStatus.SENT, realEventId);
|
|
|
|
// 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();
|
|
await room.addLiveEvents([remoteEvent], { addToState: false });
|
|
// 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)", async function () {
|
|
const eventJson = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
event: false,
|
|
});
|
|
delete eventJson["txn_id"];
|
|
delete eventJson["event_id"];
|
|
const txnId = "My_txn_id";
|
|
const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp", txn_id: txnId }, eventJson));
|
|
localEvent.status = EventStatus.SENDING;
|
|
expect(localEvent.getTxnId()).toEqual(txnId);
|
|
expect(room.timeline.length).toEqual(0);
|
|
|
|
// first add the local echo. This is done before the /send request is even sent.
|
|
room.addPendingEvent(localEvent, txnId);
|
|
expect(room.getEventForTxnId(txnId)).toEqual(localEvent);
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
// now the /sync returns the remoteEvent, it is impossible for the JS SDK to de-dupe this.
|
|
const realEventId = "$real-event-id";
|
|
const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson));
|
|
expect(remoteEvent.getUnsigned().transaction_id).toBeUndefined();
|
|
await room.addLiveEvents([remoteEvent], { addToState: false });
|
|
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.
|
|
// Now it is possible for the JS SDK to de-dupe this.
|
|
room.updatePendingEvent(localEvent, EventStatus.SENT, realEventId);
|
|
|
|
// the 2nd event should be removed from the 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 correctly handle remote echoes from other devices", async () => {
|
|
const remoteEvent = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
});
|
|
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
|
|
|
// add the remoteEvent
|
|
await room.addLiveEvents([remoteEvent], { addToState: false });
|
|
expect(room.timeline.length).toEqual(1);
|
|
});
|
|
});
|
|
|
|
describe("addEphemeralEvents", () => {
|
|
it("should call RoomState.setTypingEvent on m.typing events", function () {
|
|
const typing = utils.mkEvent({
|
|
room: roomId,
|
|
type: EventType.Typing,
|
|
event: true,
|
|
content: {
|
|
user_ids: [userA],
|
|
},
|
|
});
|
|
room.addEphemeralEvents([typing]);
|
|
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
|
|
});
|
|
});
|
|
|
|
describe("addEventsToTimeline", function () {
|
|
const events = [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "changing room name",
|
|
event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: { name: "New Room Name" },
|
|
}),
|
|
];
|
|
|
|
it("should not be able to add events to the end", function () {
|
|
expect(function () {
|
|
room.addEventsToTimeline(events, false, false, room.getLiveTimeline());
|
|
}).toThrow();
|
|
});
|
|
|
|
it("should be able to add events to the start", function () {
|
|
room.addEventsToTimeline(events, true, false, room.getLiveTimeline());
|
|
expect(room.timeline.length).toEqual(2);
|
|
expect(room.timeline[0]).toEqual(events[1]);
|
|
expect(room.timeline[1]).toEqual(events[0]);
|
|
});
|
|
|
|
it("should emit 'Room.timeline' events when added to the start", function () {
|
|
let callCount = 0;
|
|
room.on(RoomEvent.Timeline, function (event, emitRoom, toStart) {
|
|
callCount += 1;
|
|
expect(room.timeline.length).toEqual(callCount);
|
|
expect(event).toEqual(events[callCount - 1]);
|
|
expect(emitRoom).toEqual(room);
|
|
expect(toStart).toBe(true);
|
|
});
|
|
room.addEventsToTimeline(events, true, false, room.getLiveTimeline());
|
|
expect(callCount).toEqual(2);
|
|
});
|
|
});
|
|
|
|
describe("event metadata handling", function () {
|
|
it("should set event.sender for new and old events", async function () {
|
|
const sentinel = {
|
|
userId: userA,
|
|
membership: KnownMembership.Join,
|
|
name: "Alice",
|
|
} as unknown as RoomMember;
|
|
const oldSentinel = {
|
|
userId: userA,
|
|
membership: KnownMembership.Join,
|
|
name: "Old Alice",
|
|
} as unknown as RoomMember;
|
|
mocked(room.currentState.getSentinelMember).mockImplementation(function (uid) {
|
|
if (uid === userA) {
|
|
return sentinel;
|
|
}
|
|
return null;
|
|
});
|
|
mocked(room.oldState.getSentinelMember).mockImplementation(function (uid) {
|
|
if (uid === userA) {
|
|
return oldSentinel;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const newEv = utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: { name: "New Room Name" },
|
|
});
|
|
const oldEv = utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: { name: "Old Room Name" },
|
|
});
|
|
await room.addLiveEvents([newEv], { addToState: false });
|
|
expect(newEv.sender).toEqual(sentinel);
|
|
room.addEventsToTimeline([oldEv], true, false, room.getLiveTimeline());
|
|
expect(oldEv.sender).toEqual(oldSentinel);
|
|
});
|
|
|
|
it("should set event.target for new and old m.room.member events", async function () {
|
|
const sentinel = {
|
|
userId: userA,
|
|
membership: KnownMembership.Join,
|
|
name: "Alice",
|
|
} as unknown as RoomMember;
|
|
const oldSentinel = {
|
|
userId: userA,
|
|
membership: KnownMembership.Join,
|
|
name: "Old Alice",
|
|
} as unknown as RoomMember;
|
|
mocked(room.currentState.getSentinelMember).mockImplementation(function (uid) {
|
|
if (uid === userA) {
|
|
return sentinel;
|
|
}
|
|
return null;
|
|
});
|
|
mocked(room.oldState.getSentinelMember).mockImplementation(function (uid) {
|
|
if (uid === userA) {
|
|
return oldSentinel;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const newEv = utils.mkMembership({
|
|
room: roomId,
|
|
mship: KnownMembership.Invite,
|
|
user: userB,
|
|
skey: userA,
|
|
event: true,
|
|
});
|
|
const oldEv = utils.mkMembership({
|
|
room: roomId,
|
|
mship: KnownMembership.Ban,
|
|
user: userB,
|
|
skey: userA,
|
|
event: true,
|
|
});
|
|
await room.addLiveEvents([newEv], { addToState: false });
|
|
expect(newEv.target).toEqual(sentinel);
|
|
room.addEventsToTimeline([oldEv], true, false, room.getLiveTimeline());
|
|
expect(oldEv.target).toEqual(oldSentinel);
|
|
});
|
|
|
|
it(
|
|
"should call setStateEvents on the right RoomState with the right " + "forwardLooking value for old events",
|
|
function () {
|
|
const events: MatrixEvent[] = [
|
|
utils.mkMembership({
|
|
room: roomId,
|
|
mship: KnownMembership.Invite,
|
|
user: userB,
|
|
skey: userA,
|
|
event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userB,
|
|
event: true,
|
|
content: {
|
|
name: "New room",
|
|
},
|
|
}),
|
|
];
|
|
|
|
room.addEventsToTimeline(events, true, true, room.getLiveTimeline());
|
|
expect(room.oldState.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
|
expect(room.oldState.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
|
expect(events[0].forwardLooking).toBe(false);
|
|
expect(events[1].forwardLooking).toBe(false);
|
|
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|
|
|
|
const resetTimelineTests = function (timelineSupport: boolean) {
|
|
let events: MatrixEvent[];
|
|
|
|
beforeEach(function () {
|
|
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport });
|
|
// set events each time to avoid resusing Event objects (which
|
|
// doesn't work because they get frozen)
|
|
events = [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "A message",
|
|
event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: { name: "New Room Name" },
|
|
}),
|
|
utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: { name: "Another New Name" },
|
|
}),
|
|
];
|
|
});
|
|
|
|
it("should copy state from previous timeline", async function () {
|
|
await room.addLiveEvents([events[0], events[1]], { addToState: false });
|
|
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
|
|
room.resetLiveTimeline("sometoken", "someothertoken");
|
|
|
|
await room.addLiveEvents([events[2]], { addToState: false });
|
|
const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
|
|
const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
|
expect(room.getLiveTimeline().getEvents().length).toEqual(1);
|
|
expect(oldState?.getStateEvents(EventType.RoomName, "")).toEqual(events[1]);
|
|
expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]);
|
|
});
|
|
|
|
it("should reset the legacy timeline fields", async function () {
|
|
await room.addLiveEvents([events[0], events[1]], { addToState: false });
|
|
expect(room.timeline.length).toEqual(2);
|
|
|
|
const oldStateBeforeRunningReset = room.oldState;
|
|
let oldStateUpdateEmitCount = 0;
|
|
room.on(RoomEvent.OldStateUpdated, function (room, previousOldState, oldState) {
|
|
expect(previousOldState).toBe(oldStateBeforeRunningReset);
|
|
expect(oldState).toBe(room.oldState);
|
|
oldStateUpdateEmitCount += 1;
|
|
});
|
|
|
|
const currentStateBeforeRunningReset = room.currentState;
|
|
let currentStateUpdateEmitCount = 0;
|
|
room.on(RoomEvent.CurrentStateUpdated, function (room, previousCurrentState, currentState) {
|
|
expect(previousCurrentState).toBe(currentStateBeforeRunningReset);
|
|
expect(currentState).toBe(room.currentState);
|
|
currentStateUpdateEmitCount += 1;
|
|
});
|
|
|
|
room.resetLiveTimeline("sometoken", "someothertoken");
|
|
|
|
await room.addLiveEvents([events[2]], { addToState: false });
|
|
const newLiveTimeline = room.getLiveTimeline();
|
|
expect(room.timeline).toEqual(newLiveTimeline.getEvents());
|
|
expect(room.oldState).toEqual(newLiveTimeline.getState(EventTimeline.BACKWARDS));
|
|
expect(room.currentState).toEqual(newLiveTimeline.getState(EventTimeline.FORWARDS));
|
|
// Make sure `RoomEvent.OldStateUpdated` was emitted
|
|
expect(oldStateUpdateEmitCount).toEqual(1);
|
|
// Make sure `RoomEvent.OldStateUpdated` was emitted if necessary
|
|
expect(currentStateUpdateEmitCount).toEqual(timelineSupport ? 1 : 0);
|
|
});
|
|
|
|
it("should emit Room.timelineReset event and set the correct pagination token", function () {
|
|
let callCount = 0;
|
|
room.on(RoomEvent.TimelineReset, function (emitRoom) {
|
|
callCount += 1;
|
|
expect(emitRoom).toEqual(room);
|
|
|
|
// make sure that the pagination token has been set before the event is emitted.
|
|
const tok = emitRoom?.getLiveTimeline().getPaginationToken(EventTimeline.BACKWARDS);
|
|
|
|
expect(tok).toEqual("pagToken");
|
|
});
|
|
room.resetLiveTimeline("pagToken");
|
|
expect(callCount).toEqual(1);
|
|
});
|
|
|
|
it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", async function () {
|
|
await room.addLiveEvents([events[0]], { addToState: false });
|
|
expect(room.timeline.length).toEqual(1);
|
|
const firstLiveTimeline = room.getLiveTimeline();
|
|
room.resetLiveTimeline("sometoken", "someothertoken");
|
|
|
|
const tl = room.getTimelineForEvent(events[0].getId()!);
|
|
expect(tl).toBe(timelineSupport ? firstLiveTimeline : null);
|
|
});
|
|
};
|
|
|
|
describe("resetLiveTimeline with timeline support enabled", () => {
|
|
resetTimelineTests.bind(null, true);
|
|
});
|
|
describe("resetLiveTimeline with timeline support disabled", () => {
|
|
resetTimelineTests.bind(null, false);
|
|
});
|
|
|
|
describe("compareEventOrdering", function () {
|
|
beforeEach(function () {
|
|
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: true });
|
|
});
|
|
|
|
const events: MatrixEvent[] = [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "1111",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "2222",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "3333",
|
|
event: true,
|
|
}),
|
|
];
|
|
|
|
it("should handle events in the same timeline", async function () {
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
|
|
expect(
|
|
room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!),
|
|
).toBeLessThan(0);
|
|
expect(
|
|
room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, events[1].getId()!),
|
|
).toBeGreaterThan(0);
|
|
expect(
|
|
room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[1].getId()!),
|
|
).toEqual(0);
|
|
});
|
|
|
|
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, false, oldTimeline);
|
|
await room.addLiveEvents([events[1]], { addToState: false });
|
|
|
|
expect(
|
|
room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!),
|
|
).toBeLessThan(0);
|
|
expect(
|
|
room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!),
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should return null for events in non-adjacent timelines", async function () {
|
|
const oldTimeline = room.addTimeline();
|
|
|
|
room.addEventsToTimeline([events[0]], false, false, oldTimeline);
|
|
await room.addLiveEvents([events[1]], { addToState: false });
|
|
|
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)).toBe(
|
|
null,
|
|
);
|
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!)).toBe(
|
|
null,
|
|
);
|
|
});
|
|
|
|
it("should return null for unknown events", async function () {
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
|
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, "xxx")).toBe(null);
|
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering("xxx", events[0].getId()!)).toBe(null);
|
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[0].getId()!)).toBe(
|
|
0,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getJoinedMembers", function () {
|
|
it("should return members whose membership is 'join'", function () {
|
|
mocked(room.currentState.getMembers).mockImplementation(function () {
|
|
return [
|
|
{ userId: "@alice:bar", membership: KnownMembership.Join } as unknown as RoomMember,
|
|
{ userId: "@bob:bar", membership: KnownMembership.Invite } as unknown as RoomMember,
|
|
{ userId: "@cleo:bar", membership: KnownMembership.Leave } as unknown as RoomMember,
|
|
];
|
|
});
|
|
const res = room.getJoinedMembers();
|
|
expect(res.length).toEqual(1);
|
|
expect(res[0].userId).toEqual("@alice:bar");
|
|
});
|
|
|
|
it("should return an empty list if no membership is 'join'", function () {
|
|
mocked(room.currentState.getMembers).mockImplementation(function () {
|
|
return [{ userId: "@bob:bar", membership: KnownMembership.Invite } as unknown as RoomMember];
|
|
});
|
|
const res = room.getJoinedMembers();
|
|
expect(res.length).toEqual(0);
|
|
});
|
|
});
|
|
|
|
describe("hasMembershipState", function () {
|
|
it("should return true for a matching userId and membership", function () {
|
|
mocked(room.currentState.getMember).mockImplementation(function (userId) {
|
|
return {
|
|
"@alice:bar": { userId: "@alice:bar", membership: KnownMembership.Join },
|
|
"@bob:bar": { userId: "@bob:bar", membership: KnownMembership.Invite },
|
|
}[userId] as unknown as RoomMember;
|
|
});
|
|
expect(room.hasMembershipState("@bob:bar", KnownMembership.Invite)).toBe(true);
|
|
});
|
|
|
|
it("should return false if match membership but no match userId", function () {
|
|
mocked(room.currentState.getMember).mockImplementation(function (userId) {
|
|
return {
|
|
"@alice:bar": { userId: "@alice:bar", membership: KnownMembership.Join },
|
|
}[userId] as unknown as RoomMember;
|
|
});
|
|
expect(room.hasMembershipState("@bob:bar", KnownMembership.Join)).toBe(false);
|
|
});
|
|
|
|
it("should return false if match userId but no match membership", function () {
|
|
mocked(room.currentState.getMember).mockImplementation(function (userId) {
|
|
return {
|
|
"@alice:bar": { userId: "@alice:bar", membership: KnownMembership.Join },
|
|
}[userId] as unknown as RoomMember;
|
|
});
|
|
expect(room.hasMembershipState("@alice:bar", KnownMembership.Ban)).toBe(false);
|
|
});
|
|
|
|
it("should return false if no match membership or userId", function () {
|
|
mocked(room.currentState.getMember).mockImplementation(function (userId) {
|
|
return {
|
|
"@alice:bar": { userId: "@alice:bar", membership: KnownMembership.Join },
|
|
}[userId] as unknown as RoomMember;
|
|
});
|
|
expect(room.hasMembershipState("@bob:bar", KnownMembership.Invite)).toBe(false);
|
|
});
|
|
|
|
it("should return false if no members exist", function () {
|
|
expect(room.hasMembershipState("@foo:bar", KnownMembership.Join)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("recalculate", function () {
|
|
const setJoinRule = async function (rule: JoinRule) {
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkEvent({
|
|
type: EventType.RoomJoinRules,
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
join_rule: rule,
|
|
},
|
|
event: true,
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
};
|
|
const setAltAliases = async function (aliases: string[]) {
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkEvent({
|
|
type: EventType.RoomCanonicalAlias,
|
|
room: roomId,
|
|
skey: "",
|
|
content: {
|
|
alt_aliases: aliases,
|
|
},
|
|
event: true,
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
};
|
|
const setAlias = async function (alias: string) {
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkEvent({
|
|
type: EventType.RoomCanonicalAlias,
|
|
room: roomId,
|
|
skey: "",
|
|
content: { alias },
|
|
event: true,
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
};
|
|
const setRoomName = async function (name: string) {
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkEvent({
|
|
type: EventType.RoomName,
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
name: name,
|
|
},
|
|
event: true,
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
};
|
|
const addMember = async function (userId: string, state = KnownMembership.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);
|
|
await room.addLiveEvents([event], { addToState: true });
|
|
return event;
|
|
};
|
|
|
|
beforeEach(function () {
|
|
// no mocking
|
|
room = new Room(roomId, new TestClient(userA).client, userA);
|
|
});
|
|
|
|
describe("Room.recalculate => Stripped State Events", function () {
|
|
it(
|
|
"should set stripped state events as actual state events if the " + "room is an invite room",
|
|
async function () {
|
|
const roomName = "flibble";
|
|
|
|
const event = await addMember(userA, KnownMembership.Invite);
|
|
event.event.unsigned = {};
|
|
event.event.unsigned.invite_room_state = [
|
|
{
|
|
type: EventType.RoomName,
|
|
state_key: "",
|
|
content: {
|
|
name: roomName,
|
|
},
|
|
sender: "@bob:foobar",
|
|
},
|
|
];
|
|
|
|
room.recalculate();
|
|
expect(room.name).toEqual(roomName);
|
|
},
|
|
);
|
|
|
|
it("should not clobber state events if it isn't an invite room", async function () {
|
|
const event = await addMember(userA, KnownMembership.Join);
|
|
const roomName = "flibble";
|
|
setRoomName(roomName);
|
|
const roomNameToIgnore = "ignoreme";
|
|
event.event.unsigned = {};
|
|
event.event.unsigned.invite_room_state = [
|
|
{
|
|
type: EventType.RoomName,
|
|
state_key: "",
|
|
content: {
|
|
name: roomNameToIgnore,
|
|
},
|
|
sender: "@bob:foobar",
|
|
},
|
|
];
|
|
|
|
room.recalculate();
|
|
expect(room.name).toEqual(roomName);
|
|
});
|
|
});
|
|
|
|
describe("Room.recalculate => Room Name using room summary", function () {
|
|
it("should use room heroes if available", function () {
|
|
addMember(userA, KnownMembership.Invite);
|
|
addMember(userB);
|
|
addMember(userC);
|
|
addMember(userD);
|
|
room.setSummary({
|
|
"m.heroes": [userB, userC, userD],
|
|
});
|
|
|
|
room.recalculate();
|
|
expect(room.name).toEqual(`${userB} and 2 others`);
|
|
});
|
|
|
|
it("missing hero member state reverts to mxid", function () {
|
|
room.setSummary({
|
|
"m.heroes": [userB],
|
|
"m.joined_member_count": 2,
|
|
});
|
|
|
|
room.recalculate();
|
|
expect(room.name).toEqual(userB);
|
|
});
|
|
|
|
it("uses hero name from state", function () {
|
|
const name = "Mr B";
|
|
addMember(userA, KnownMembership.Invite);
|
|
addMember(userB, KnownMembership.Join, { name });
|
|
room.setSummary({
|
|
"m.heroes": [userB],
|
|
});
|
|
|
|
room.recalculate();
|
|
expect(room.name).toEqual(name);
|
|
});
|
|
|
|
it("uses counts from summary", function () {
|
|
const name = "Mr B";
|
|
addMember(userB, KnownMembership.Join, { name });
|
|
room.setSummary({
|
|
"m.heroes": [userB],
|
|
"m.joined_member_count": 50,
|
|
"m.invited_member_count": 50,
|
|
});
|
|
room.recalculate();
|
|
expect(room.name).toEqual(`${name} and 98 others`);
|
|
});
|
|
|
|
it("relies on heroes in case of absent counts", function () {
|
|
const nameB = "Mr Bean";
|
|
const nameC = "Mel C";
|
|
addMember(userB, KnownMembership.Join, { name: nameB });
|
|
addMember(userC, KnownMembership.Join, { name: nameC });
|
|
room.setSummary({
|
|
"m.heroes": [userB, userC],
|
|
});
|
|
room.recalculate();
|
|
expect(room.name).toEqual(`${nameB} and ${nameC}`);
|
|
});
|
|
|
|
it("uses only heroes", function () {
|
|
const nameB = "Mr Bean";
|
|
addMember(userB, KnownMembership.Join, { name: nameB });
|
|
addMember(userC, KnownMembership.Join);
|
|
room.setSummary({
|
|
"m.heroes": [userB],
|
|
});
|
|
room.recalculate();
|
|
expect(room.name).toEqual(nameB);
|
|
});
|
|
|
|
it("supports MSC4186 style heroes", () => {
|
|
const nameB = "Bertha Bobbington";
|
|
const nameC = "Clarissa Harissa";
|
|
addMember(userB, KnownMembership.Join, { name: nameB });
|
|
addMember(userC, KnownMembership.Join, { name: nameC });
|
|
room.setMSC4186SummaryData([{ user_id: userB }, { user_id: userC }], undefined, undefined);
|
|
room.recalculate();
|
|
expect(room.name).toEqual(`${nameB} and ${nameC}`);
|
|
});
|
|
|
|
it("reverts to empty room in case of self chat", function () {
|
|
room.setSummary({
|
|
"m.heroes": [],
|
|
"m.invited_member_count": 1,
|
|
});
|
|
room.recalculate();
|
|
expect(room.name).toEqual("Empty room");
|
|
});
|
|
|
|
it("emits an update event", function () {
|
|
const spy = jest.fn();
|
|
const summary = {
|
|
"m.heroes": [],
|
|
"m.invited_member_count": 1,
|
|
};
|
|
|
|
room.once(RoomEvent.Summary, spy);
|
|
|
|
room.setSummary(summary);
|
|
room.recalculate();
|
|
|
|
expect(spy).toHaveBeenCalledWith(summary);
|
|
});
|
|
});
|
|
|
|
describe("Room.recalculate => Room Name", function () {
|
|
it(
|
|
"should return the names of members in a private (invite join_rules)" +
|
|
" room if a room name and alias don't exist and there are >3 members.",
|
|
function () {
|
|
setJoinRule(JoinRule.Invite);
|
|
addMember(userA);
|
|
addMember(userB);
|
|
addMember(userC);
|
|
addMember(userD);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
// we expect at least 1 member to be mentioned
|
|
const others = [userB, userC, userD];
|
|
let found = false;
|
|
for (let i = 0; i < others.length; i++) {
|
|
if (name.indexOf(others[i]) !== -1) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
expect(found).toEqual(true);
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should return the names of members in a private (invite join_rules)" +
|
|
" room if a room name and alias don't exist and there are >2 members.",
|
|
function () {
|
|
setJoinRule(JoinRule.Invite);
|
|
addMember(userA);
|
|
addMember(userB);
|
|
addMember(userC);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name.indexOf(userB)).not.toEqual(-1);
|
|
expect(name.indexOf(userC)).not.toEqual(-1);
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should return the names of members in a public (public join_rules)" +
|
|
" room if a room name and alias don't exist and there are >2 members.",
|
|
function () {
|
|
setJoinRule(JoinRule.Public);
|
|
addMember(userA);
|
|
addMember(userB);
|
|
addMember(userC);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name.indexOf(userB)).not.toEqual(-1);
|
|
expect(name.indexOf(userC)).not.toEqual(-1);
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should show the other user's name for public (public join_rules)" +
|
|
" rooms if a room name and alias don't exist and it is a 1:1-chat.",
|
|
function () {
|
|
setJoinRule(JoinRule.Public);
|
|
addMember(userA);
|
|
addMember(userB);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name.indexOf(userB)).not.toEqual(-1);
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should show the other user's name for private " +
|
|
"(invite join_rules) rooms if a room name and alias don't exist and it" +
|
|
" is a 1:1-chat.",
|
|
function () {
|
|
setJoinRule(JoinRule.Invite);
|
|
addMember(userA);
|
|
addMember(userB);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name.indexOf(userB)).not.toEqual(-1);
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should show the other user's name for private" +
|
|
" (invite join_rules) rooms if you are invited to it.",
|
|
function () {
|
|
setJoinRule(JoinRule.Invite);
|
|
addMember(userA, KnownMembership.Invite, { user: userB });
|
|
addMember(userB);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name.indexOf(userB)).not.toEqual(-1);
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should show the room alias if one exists for private " +
|
|
"(invite join_rules) rooms if a room name doesn't exist.",
|
|
function () {
|
|
const alias = "#room_alias:here";
|
|
setJoinRule(JoinRule.Invite);
|
|
setAlias(alias);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).toEqual(alias);
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should show the room alias if one exists for public " +
|
|
"(public join_rules) rooms if a room name doesn't exist.",
|
|
function () {
|
|
const alias = "#room_alias:here";
|
|
setJoinRule(JoinRule.Public);
|
|
setAlias(alias);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).toEqual(alias);
|
|
},
|
|
);
|
|
|
|
it("should not show alt aliases if a room name does not exist", () => {
|
|
const alias = "#room_alias:here";
|
|
setAltAliases([alias, "#another:here"]);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).not.toEqual(alias);
|
|
});
|
|
|
|
it("should show the room name if one exists for private " + "(invite join_rules) rooms.", function () {
|
|
const roomName = "A mighty name indeed";
|
|
setJoinRule(JoinRule.Invite);
|
|
setRoomName(roomName);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).toEqual(roomName);
|
|
});
|
|
|
|
it("should show the room name if one exists for public " + "(public join_rules) rooms.", function () {
|
|
const roomName = "A mighty name indeed";
|
|
setJoinRule(JoinRule.Public);
|
|
setRoomName(roomName);
|
|
room.recalculate();
|
|
expect(room.name).toEqual(roomName);
|
|
});
|
|
|
|
it(
|
|
"should return 'Empty room' for private (invite join_rules) rooms if" +
|
|
" a room name and alias don't exist and it is a self-chat.",
|
|
function () {
|
|
setJoinRule(JoinRule.Invite);
|
|
addMember(userA);
|
|
room.recalculate();
|
|
expect(room.name).toEqual("Empty room");
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should return 'Empty room' for public (public join_rules) rooms if a" +
|
|
" room name and alias don't exist and it is a self-chat.",
|
|
function () {
|
|
setJoinRule(JoinRule.Public);
|
|
addMember(userA);
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).toEqual("Empty room");
|
|
},
|
|
);
|
|
|
|
it("should return 'Empty room' if there is no name, " + "alias or members in the room.", function () {
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).toEqual("Empty room");
|
|
});
|
|
|
|
it("should return '[inviter display name] if state event " + "available", function () {
|
|
setJoinRule(JoinRule.Invite);
|
|
addMember(userB, KnownMembership.Join, { name: "Alice" });
|
|
addMember(userA, KnownMembership.Invite, { user: userA });
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).toEqual("Alice");
|
|
});
|
|
|
|
it("should return inviter mxid if display name not available", function () {
|
|
setJoinRule(JoinRule.Invite);
|
|
addMember(userB);
|
|
addMember(userA, KnownMembership.Invite, { user: userA });
|
|
room.recalculate();
|
|
const name = room.name;
|
|
expect(name).toEqual(userB);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("receipts", function () {
|
|
const eventToAck = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "PLEASE ACKNOWLEDGE MY EXISTENCE",
|
|
event: true,
|
|
});
|
|
|
|
function mkReceipt(roomId: string, records: Array<ReturnType<typeof mkRecord>>) {
|
|
const content: IContent = {};
|
|
records.forEach(function (r) {
|
|
if (!content[r.eventId]) {
|
|
content[r.eventId] = {};
|
|
}
|
|
if (!content[r.eventId][r.type]) {
|
|
content[r.eventId][r.type] = {};
|
|
}
|
|
content[r.eventId][r.type][r.userId] = {
|
|
ts: r.ts,
|
|
};
|
|
});
|
|
return new MatrixEvent({
|
|
content: content,
|
|
room_id: roomId,
|
|
type: "m.receipt",
|
|
});
|
|
}
|
|
|
|
function mkRecord(eventId: string, type: string, userId: string, ts: number) {
|
|
ts = ts || Date.now();
|
|
return {
|
|
eventId: eventId,
|
|
type: type,
|
|
userId: userId,
|
|
ts: ts,
|
|
};
|
|
}
|
|
|
|
describe("addReceipt", function () {
|
|
describe("resets the unread count", () => {
|
|
const event1 = utils.mkMessage({ room: roomId, user: userA, msg: "1", event: true });
|
|
const event2 = utils.mkMessage({ room: roomId, user: userA, msg: "2", event: true });
|
|
|
|
it("should reset the unread count when our non-synthetic receipt points to the latest event", () => {
|
|
// Given a room with 2 events, and an unread count set.
|
|
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
|
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
|
room.setUnread(NotificationCountType.Total, 45);
|
|
room.setUnread(NotificationCountType.Highlight, 57);
|
|
// Sanity check:
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(45);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(57);
|
|
|
|
// When I receive a receipt for me for the last event
|
|
const receipt = mkReceipt(roomId, [mkRecord(event2.getId()!, "m.read", userA, 123)]);
|
|
room.addReceipt(receipt);
|
|
|
|
// Then the count is set to 0
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(0);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(0);
|
|
});
|
|
|
|
it("should not reset the unread count when someone else's receipt points to the latest event", () => {
|
|
// Given a room with 2 events, and an unread count set.
|
|
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
|
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
|
room.setUnread(NotificationCountType.Total, 45);
|
|
room.setUnread(NotificationCountType.Highlight, 57);
|
|
// Sanity check:
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(45);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(57);
|
|
|
|
// When I receive a receipt for someone else for the last event
|
|
const receipt = mkReceipt(roomId, [mkRecord(event2.getId()!, "m.read", userB, 123)]);
|
|
room.addReceipt(receipt);
|
|
|
|
// Then the count is unchanged because it's not my receipt
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(45);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(57);
|
|
});
|
|
|
|
it("should not reset the unread count when our non-synthetic receipt points to an earlier event", () => {
|
|
// Given a room with 2 events, and an unread count set.
|
|
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
|
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
|
room.setUnread(NotificationCountType.Total, 45);
|
|
room.setUnread(NotificationCountType.Highlight, 57);
|
|
// Sanity check:
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(45);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(57);
|
|
|
|
// When I receive a receipt for me for an earlier event
|
|
const receipt = mkReceipt(roomId, [mkRecord(event1.getId()!, "m.read", userA, 123)]);
|
|
room.addReceipt(receipt);
|
|
|
|
// Then the count is unchanged because it wasn't the latest event
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(45);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(57);
|
|
});
|
|
|
|
it("should not reset the unread count when our a synthetic receipt points to the latest event", () => {
|
|
// Given a room with 2 events, and an unread count set.
|
|
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
|
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
|
room.setUnread(NotificationCountType.Total, 45);
|
|
room.setUnread(NotificationCountType.Highlight, 57);
|
|
// Sanity check:
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(45);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(57);
|
|
|
|
// When I receive a synthetic receipt for me for the last event
|
|
const receipt = mkReceipt(roomId, [mkRecord(event2.getId()!, "m.read", userA, 123)]);
|
|
room.addReceipt(receipt, true);
|
|
|
|
// Then the count is unchanged because the receipt was synthetic
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(45);
|
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(57);
|
|
});
|
|
});
|
|
|
|
it("should store the receipt so it can be obtained via getReceiptsForEvent", function () {
|
|
const ts = 13787898424;
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(eventToAck.getId()!, "m.read", userB, ts)]));
|
|
expect(room.getReceiptsForEvent(eventToAck)).toEqual([
|
|
{
|
|
type: "m.read",
|
|
userId: userB,
|
|
data: {
|
|
ts: ts,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("should emit an event when a receipt is added", function () {
|
|
const listener = jest.fn();
|
|
room.on(RoomEvent.Receipt, listener);
|
|
|
|
const ts = 13787898424;
|
|
|
|
const receiptEvent = mkReceipt(roomId, [mkRecord(eventToAck.getId()!, "m.read", userB, ts)]);
|
|
|
|
room.addReceipt(receiptEvent);
|
|
expect(listener).toHaveBeenCalledWith(receiptEvent, room);
|
|
});
|
|
|
|
it("should clobber receipts based on type and user ID", function () {
|
|
const nextEventToAck = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "I AM HERE YOU KNOW",
|
|
event: true,
|
|
});
|
|
const ts = 13787898424;
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(eventToAck.getId()!, "m.read", userB, ts)]));
|
|
const ts2 = 13787899999;
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(nextEventToAck.getId()!, "m.read", userB, ts2)]));
|
|
expect(room.getReceiptsForEvent(eventToAck)).toEqual([]);
|
|
expect(room.getReceiptsForEvent(nextEventToAck)).toEqual([
|
|
{
|
|
type: "m.read",
|
|
userId: userB,
|
|
data: {
|
|
ts: ts2,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("should persist multiple receipts for a single event ID", function () {
|
|
const ts = 13787898424;
|
|
room.addReceipt(
|
|
mkReceipt(roomId, [
|
|
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
|
mkRecord(eventToAck.getId()!, "m.read", userC, ts),
|
|
mkRecord(eventToAck.getId()!, "m.read", userD, ts),
|
|
]),
|
|
);
|
|
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB, userC, userD]);
|
|
});
|
|
|
|
it("should persist multiple receipts for a single receipt type", function () {
|
|
const eventTwo = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "2222",
|
|
event: true,
|
|
});
|
|
const eventThree = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "3333",
|
|
event: true,
|
|
});
|
|
const ts = 13787898424;
|
|
room.addReceipt(
|
|
mkReceipt(roomId, [
|
|
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
|
mkRecord(eventTwo.getId()!, "m.read", userC, ts),
|
|
mkRecord(eventThree.getId()!, "m.read", userD, ts),
|
|
]),
|
|
);
|
|
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
|
expect(room.getUsersReadUpTo(eventTwo)).toEqual([userC]);
|
|
expect(room.getUsersReadUpTo(eventThree)).toEqual([userD]);
|
|
});
|
|
|
|
it("should persist multiple receipts for a single user ID", function () {
|
|
room.addReceipt(
|
|
mkReceipt(roomId, [
|
|
mkRecord(eventToAck.getId()!, "m.delivered", userB, 13787898424),
|
|
mkRecord(eventToAck.getId()!, "m.read", userB, 22222222),
|
|
mkRecord(eventToAck.getId()!, "m.seen", userB, 33333333),
|
|
]),
|
|
);
|
|
expect(room.getReceiptsForEvent(eventToAck)).toEqual([
|
|
{
|
|
type: "m.delivered",
|
|
userId: userB,
|
|
data: {
|
|
ts: 13787898424,
|
|
},
|
|
},
|
|
{
|
|
type: "m.read",
|
|
userId: userB,
|
|
data: {
|
|
ts: 22222222,
|
|
},
|
|
},
|
|
{
|
|
type: "m.seen",
|
|
userId: userB,
|
|
data: {
|
|
ts: 33333333,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("should prioritise the most recent event", async function () {
|
|
const events: MatrixEvent[] = [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "1111",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "2222",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "3333",
|
|
event: true,
|
|
}),
|
|
];
|
|
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
const ts = 13787898424;
|
|
|
|
// check it initialises correctly
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[0].getId()!, "m.read", userB, ts)]));
|
|
expect(room.getEventReadUpTo(userB)).toEqual(events[0].getId());
|
|
|
|
// 2>0, so it should move forward
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[2].getId()!, "m.read", userB, ts)]));
|
|
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
|
|
|
// 1<2, so it should stay put
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[1].getId()!, "m.read", userB, ts)]));
|
|
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
|
});
|
|
|
|
it("should prioritise the most recent event even if it is synthetic", async () => {
|
|
const events: MatrixEvent[] = [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "1111",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "2222",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "3333",
|
|
event: true,
|
|
}),
|
|
];
|
|
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
const ts = 13787898424;
|
|
|
|
// check it initialises correctly
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[0].getId()!, "m.read", userB, ts)]));
|
|
expect(room.getEventReadUpTo(userB)).toEqual(events[0].getId());
|
|
|
|
// 2>0, so it should move forward
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[2].getId()!, "m.read", userB, ts)]), true);
|
|
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
|
expect(room.getReceiptsForEvent(events[2])).toEqual([{ data: { ts }, type: "m.read", userId: userB }]);
|
|
|
|
// 1<2, so it should stay put
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[1].getId()!, "m.read", userB, ts)]));
|
|
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
|
expect(room.getEventReadUpTo(userB, true)).toEqual(events[1].getId());
|
|
expect(room.getReceiptsForEvent(events[2])).toEqual([{ data: { ts }, type: "m.read", userId: userB }]);
|
|
});
|
|
});
|
|
|
|
describe("getUsersReadUpTo", function () {
|
|
it("should return user IDs read up to the given event", function () {
|
|
const ts = 13787898424;
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(eventToAck.getId()!, "m.read", userB, ts)]));
|
|
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
|
});
|
|
});
|
|
|
|
describe("hasUserReadUpTo", function () {
|
|
it("returns true if there is a receipt for this event (main timeline)", function () {
|
|
const ts = 13787898424;
|
|
room.addLiveEvents([eventToAck], { addToState: false });
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(eventToAck.getId()!, "m.read", userB, ts)]));
|
|
room.findEventById = jest.fn().mockReturnValue({ getThread: jest.fn() } as unknown as MatrixEvent);
|
|
expect(room.hasUserReadEvent(userB, eventToAck.getId()!)).toEqual(true);
|
|
});
|
|
|
|
it("returns true if there is a receipt for a later event (main timeline)", async function () {
|
|
// Given some events exist in the room
|
|
const events: MatrixEvent[] = [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "1111",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "2222",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "3333",
|
|
event: true,
|
|
}),
|
|
];
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
|
|
// When I add a receipt for the latest one
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[2].getId()!, "m.read", userB, 102)]));
|
|
|
|
// Then the older ones are read too
|
|
expect(room.hasUserReadEvent(userB, events[0].getId()!)).toEqual(true);
|
|
expect(room.hasUserReadEvent(userB, events[1].getId()!)).toEqual(true);
|
|
});
|
|
|
|
describe("threads enabled", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(room.client, "supportsThreads").mockReturnValue(true);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it("returns true if there is an unthreaded receipt for a later event in a thread", async () => {
|
|
// Given a thread exists in the room
|
|
const { thread, events } = mkThread({ room, length: 3 });
|
|
thread.initialEventsFetched = true;
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
|
|
// When I add an unthreaded receipt for the latest thread message
|
|
room.addReceipt(mkReceipt(roomId, [mkRecord(events[2].getId()!, "m.read", userB, 102)]));
|
|
|
|
// Then the main timeline message is read
|
|
expect(room.hasUserReadEvent(userB, events[0].getId()!)).toEqual(true);
|
|
});
|
|
});
|
|
|
|
it("returns false for an unknown event", function () {
|
|
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("tags", function () {
|
|
function mkTags(roomId: string, tags: object) {
|
|
const content = { tags: tags };
|
|
return new MatrixEvent({
|
|
content: content,
|
|
room_id: roomId,
|
|
type: "m.tag",
|
|
});
|
|
}
|
|
|
|
describe("addTag", function () {
|
|
it(
|
|
"should set tags on rooms from event stream so " + "they can be obtained by the tags property",
|
|
function () {
|
|
const tags = { "m.foo": { order: 0.5 } };
|
|
room.addTags(mkTags(roomId, tags));
|
|
expect(room.tags).toEqual(tags);
|
|
},
|
|
);
|
|
|
|
it("should emit Room.tags event when new tags are " + "received on the event stream", function () {
|
|
const listener = jest.fn();
|
|
room.on(RoomEvent.Tags, listener);
|
|
|
|
const tags = { "m.foo": { order: 0.5 } };
|
|
const event = mkTags(roomId, tags);
|
|
room.addTags(event);
|
|
expect(listener).toHaveBeenCalledWith(event, room);
|
|
});
|
|
|
|
// XXX: shouldn't we try injecting actual m.tag events onto the eventstream
|
|
// rather than injecting via room.addTags()?
|
|
});
|
|
});
|
|
|
|
describe("addPendingEvent", 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, {
|
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
});
|
|
const eventA = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "remote 1",
|
|
event: true,
|
|
});
|
|
const eventB = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "local 1",
|
|
event: true,
|
|
});
|
|
eventB.status = EventStatus.SENDING;
|
|
const eventC = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "remote 2",
|
|
event: true,
|
|
});
|
|
await room.addLiveEvents([eventA], { addToState: false });
|
|
room.addPendingEvent(eventB, "TXN1");
|
|
await room.addLiveEvents([eventC], { addToState: false });
|
|
expect(room.timeline).toEqual([eventA, eventC]);
|
|
expect(room.getPendingEvents()).toEqual([eventB]);
|
|
},
|
|
);
|
|
|
|
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,
|
|
});
|
|
const eventA = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "remote 1",
|
|
event: true,
|
|
});
|
|
const eventB = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "local 1",
|
|
event: true,
|
|
});
|
|
eventB.status = EventStatus.SENDING;
|
|
const eventC = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "remote 2",
|
|
event: true,
|
|
});
|
|
await room.addLiveEvents([eventA], { addToState: false });
|
|
room.addPendingEvent(eventB, "TXN1");
|
|
await room.addLiveEvents([eventC], { addToState: false });
|
|
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;
|
|
const room = new Room(roomId, client, userA, {
|
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
});
|
|
|
|
const eventA = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
msg: "remote 1",
|
|
event: true,
|
|
});
|
|
eventA.status = EventStatus.SENDING;
|
|
const redactA = utils.mkEvent({
|
|
room: roomId,
|
|
user: userA,
|
|
type: EventType.RoomRedaction,
|
|
content: {},
|
|
redacts: eventA.getId()!,
|
|
event: true,
|
|
});
|
|
redactA.status = EventStatus.SENDING;
|
|
|
|
room.addPendingEvent(eventA, "TXN1");
|
|
expect(room.getPendingEvents()).toEqual([eventA]);
|
|
room.addPendingEvent(redactA, "TXN2");
|
|
expect(room.getPendingEvents()).toEqual([eventA, redactA]);
|
|
expect(eventA.isRedacted()).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("updatePendingEvent", function () {
|
|
it("should remove cancelled events from the pending list", function () {
|
|
const client = new TestClient("@alice:example.com", "alicedevice").client;
|
|
const room = new Room(roomId, client, userA, {
|
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
});
|
|
const eventA = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
});
|
|
eventA.status = EventStatus.SENDING;
|
|
const eventId = eventA.getId();
|
|
|
|
room.addPendingEvent(eventA, "TXN1");
|
|
expect(room.getPendingEvents()).toEqual([eventA]);
|
|
|
|
// the event has to have been failed or queued before it can be
|
|
// cancelled
|
|
room.updatePendingEvent(eventA, EventStatus.NOT_SENT);
|
|
|
|
let callCount = 0;
|
|
room.on(RoomEvent.LocalEchoUpdated, function (event, emitRoom, oldEventId, oldStatus) {
|
|
expect(event).toEqual(eventA);
|
|
expect(event.status).toEqual(EventStatus.CANCELLED);
|
|
expect(emitRoom).toEqual(room);
|
|
expect(oldEventId).toEqual(eventId);
|
|
expect(oldStatus).toEqual(EventStatus.NOT_SENT);
|
|
callCount++;
|
|
});
|
|
|
|
room.updatePendingEvent(eventA, EventStatus.CANCELLED);
|
|
expect(room.getPendingEvents()).toEqual([]);
|
|
expect(callCount).toEqual(1);
|
|
});
|
|
|
|
it("should remove cancelled events from the timeline", function () {
|
|
const room = new Room(roomId, null!, userA);
|
|
const eventA = utils.mkMessage({
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
});
|
|
eventA.status = EventStatus.SENDING;
|
|
const eventId = eventA.getId();
|
|
|
|
room.addPendingEvent(eventA, "TXN1");
|
|
expect(room.getLiveTimeline().getEvents()).toEqual([eventA]);
|
|
|
|
// the event has to have been failed or queued before it can be
|
|
// cancelled
|
|
room.updatePendingEvent(eventA, EventStatus.NOT_SENT);
|
|
|
|
let callCount = 0;
|
|
room.on(RoomEvent.LocalEchoUpdated, function (event, emitRoom, oldEventId, oldStatus) {
|
|
expect(event).toEqual(eventA);
|
|
expect(event.status).toEqual(EventStatus.CANCELLED);
|
|
expect(emitRoom).toEqual(room);
|
|
expect(oldEventId).toEqual(eventId);
|
|
expect(oldStatus).toEqual(EventStatus.NOT_SENT);
|
|
callCount++;
|
|
});
|
|
|
|
room.updatePendingEvent(eventA, EventStatus.CANCELLED);
|
|
expect(room.getLiveTimeline().getEvents()).toEqual([]);
|
|
expect(callCount).toEqual(1);
|
|
});
|
|
});
|
|
|
|
describe("loadMembersIfNeeded", function () {
|
|
function createClientMock(
|
|
serverResponse: Error | MatrixEvent[],
|
|
storageResponse: MatrixEvent[] | Error | null = null,
|
|
) {
|
|
return {
|
|
getEventMapper: function () {
|
|
// events should already be MatrixEvents
|
|
return function (event: MatrixEvent) {
|
|
return event;
|
|
};
|
|
},
|
|
isCryptoEnabled() {
|
|
return true;
|
|
},
|
|
isRoomEncrypted: function () {
|
|
return false;
|
|
},
|
|
members: jest.fn().mockImplementation(() => {
|
|
if (serverResponse instanceof Error) {
|
|
return Promise.reject(serverResponse);
|
|
} else {
|
|
return Promise.resolve({ chunk: serverResponse });
|
|
}
|
|
}),
|
|
store: {
|
|
storageResponse,
|
|
storedMembers: [] as IStateEventWithRoomId[] | null,
|
|
getOutOfBandMembers: function () {
|
|
if (this.storageResponse instanceof Error) {
|
|
return Promise.reject(this.storageResponse);
|
|
} else {
|
|
return Promise.resolve(this.storageResponse);
|
|
}
|
|
},
|
|
setOutOfBandMembers: function (roomId: string, memberEvents: IStateEventWithRoomId[]) {
|
|
this.storedMembers = memberEvents;
|
|
return Promise.resolve();
|
|
},
|
|
getSyncToken: () => "sync_token",
|
|
getPendingEvents: jest.fn().mockResolvedValue([]),
|
|
setPendingEvents: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
};
|
|
}
|
|
|
|
const memberEvent = utils.mkMembership({
|
|
user: "@user_a:bar",
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
});
|
|
|
|
it("should load members from server on first call", async function () {
|
|
const client = createClientMock([memberEvent]);
|
|
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
|
await room.loadMembersIfNeeded();
|
|
const memberA = room.getMember("@user_a:bar")!;
|
|
expect(memberA.name).toEqual("User A");
|
|
const storedMembers = client.store.storedMembers!;
|
|
expect(storedMembers.length).toEqual(1);
|
|
expect(storedMembers[0].event_id).toEqual(memberEvent.getId());
|
|
});
|
|
|
|
it("should take members from storage if available", async function () {
|
|
const memberEvent2 = utils.mkMembership({
|
|
user: "@user_a:bar",
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Ms A",
|
|
});
|
|
const client = createClientMock([memberEvent2], [memberEvent]);
|
|
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
|
|
|
await room.loadMembersIfNeeded();
|
|
|
|
const memberA = room.getMember("@user_a:bar")!;
|
|
expect(memberA.name).toEqual("User A");
|
|
});
|
|
|
|
it("should allow retry on error", async function () {
|
|
const client = createClientMock(new Error("server says no"));
|
|
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
|
await expect(room.loadMembersIfNeeded()).rejects.toBeTruthy();
|
|
|
|
client.members.mockReturnValue({ chunk: [memberEvent] });
|
|
await room.loadMembersIfNeeded();
|
|
const memberA = room.getMember("@user_a:bar")!;
|
|
expect(memberA.name).toEqual("User A");
|
|
});
|
|
});
|
|
|
|
describe("getMyMembership", function () {
|
|
it("should return synced membership if membership isn't available yet", function () {
|
|
const room = new Room(roomId, null!, userA);
|
|
room.updateMyMembership(KnownMembership.Invite);
|
|
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
|
|
});
|
|
it("should emit a Room.myMembership event on a change", function () {
|
|
const room = new Room(roomId, null!, userA);
|
|
const events: {
|
|
membership: string;
|
|
oldMembership?: string;
|
|
}[] = [];
|
|
room.on(RoomEvent.MyMembership, (_room, membership, oldMembership) => {
|
|
events.push({ membership, oldMembership });
|
|
});
|
|
room.updateMyMembership(KnownMembership.Invite);
|
|
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
|
|
expect(events[0]).toEqual({ membership: KnownMembership.Invite, oldMembership: undefined });
|
|
events.splice(0); //clear
|
|
room.updateMyMembership(KnownMembership.Invite);
|
|
expect(events.length).toEqual(0);
|
|
room.updateMyMembership(KnownMembership.Join);
|
|
expect(room.getMyMembership()).toEqual(KnownMembership.Join);
|
|
expect(events[0]).toEqual({ membership: KnownMembership.Join, oldMembership: KnownMembership.Invite });
|
|
});
|
|
});
|
|
|
|
describe("getDMInviter", () => {
|
|
it("should delegate to RoomMember::getDMInviter if available", () => {
|
|
const room = new Room(roomId, null!, userA);
|
|
room.currentState.markOutOfBandMembersStarted();
|
|
room.currentState.setOutOfBandMembers([
|
|
new MatrixEvent({
|
|
type: EventType.RoomMember,
|
|
state_key: userA,
|
|
sender: userB,
|
|
content: {
|
|
membership: KnownMembership.Invite,
|
|
is_direct: true,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
expect(room.getDMInviter()).toBe(userB);
|
|
});
|
|
|
|
it("should fall back to summary heroes and return the first one", () => {
|
|
const room = new Room(roomId, null!, userA);
|
|
room.updateMyMembership(KnownMembership.Invite);
|
|
room.setSummary({
|
|
"m.heroes": [userA, userC],
|
|
"m.joined_member_count": 1,
|
|
"m.invited_member_count": 1,
|
|
});
|
|
|
|
expect(room.getDMInviter()).toBe(userC);
|
|
});
|
|
|
|
it("should return undefined if we're not joined or invited to the room", () => {
|
|
const room = new Room(roomId, null!, userA);
|
|
expect(room.getDMInviter()).toBeUndefined();
|
|
room.updateMyMembership(KnownMembership.Leave);
|
|
expect(room.getDMInviter()).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("guessDMUserId", function () {
|
|
it("should return first hero id", function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
room.setSummary({
|
|
"m.heroes": [userB],
|
|
"m.joined_member_count": 1,
|
|
"m.invited_member_count": 1,
|
|
});
|
|
expect(room.guessDMUserId()).toEqual(userB);
|
|
});
|
|
it("should return first member that isn't self", async function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.guessDMUserId()).toEqual(userB);
|
|
});
|
|
it("should return self if only member present", function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
expect(room.guessDMUserId()).toEqual(userA);
|
|
});
|
|
});
|
|
|
|
describe("getAvatarFallbackMember", () => {
|
|
it("should return undefined if the room isn't a 1:1", () => {
|
|
const room = new Room(roomId, null!, userA);
|
|
room.currentState.setJoinedMemberCount(2);
|
|
room.currentState.setInvitedMemberCount(1);
|
|
expect(room.getAvatarFallbackMember()).toBeUndefined();
|
|
});
|
|
|
|
it("should use summary heroes member if 1:1", () => {
|
|
const room = new Room(roomId, null!, userA);
|
|
room.currentState.markOutOfBandMembersStarted();
|
|
room.currentState.setOutOfBandMembers([
|
|
new MatrixEvent({
|
|
type: EventType.RoomMember,
|
|
state_key: userD,
|
|
sender: userD,
|
|
content: {
|
|
membership: KnownMembership.Join,
|
|
},
|
|
}),
|
|
]);
|
|
room.setSummary({
|
|
"m.heroes": [userA, userD],
|
|
"m.joined_member_count": 1,
|
|
"m.invited_member_count": 1,
|
|
});
|
|
expect(room.getAvatarFallbackMember()?.userId).toBe(userD);
|
|
});
|
|
|
|
it("should return undefined if the room is a 1:1 plus functional member", async function () {
|
|
const room = new Room(roomId, null!, userA);
|
|
await room.currentState.setStateEvents([
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: "join",
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: "join",
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
service_members: [userB],
|
|
},
|
|
}),
|
|
]);
|
|
expect(room.getAvatarFallbackMember()).toBeUndefined();
|
|
});
|
|
|
|
it("should pick nonfunctional member from summary heroes if room is a 1:1 plus functional member", async function () {
|
|
const room = new Room(roomId, null!, userA);
|
|
await room.currentState.setStateEvents([
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: "join",
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: "join",
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userD,
|
|
mship: "join",
|
|
room: roomId,
|
|
event: true,
|
|
name: "User D",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
service_members: [userB],
|
|
},
|
|
}),
|
|
]);
|
|
room.setSummary({
|
|
"m.heroes": [userA, userD, userB],
|
|
"m.joined_member_count": 2,
|
|
"m.invited_member_count": 1,
|
|
});
|
|
expect(room.getAvatarFallbackMember()?.userId).toBe(userD);
|
|
});
|
|
});
|
|
|
|
describe("maySendMessage", function () {
|
|
it("should return false if synced membership not join", function () {
|
|
const room = new Room(roomId, { isRoomEncrypted: () => false } as any, userA);
|
|
room.updateMyMembership(KnownMembership.Invite);
|
|
expect(room.maySendMessage()).toEqual(false);
|
|
room.updateMyMembership(KnownMembership.Leave);
|
|
expect(room.maySendMessage()).toEqual(false);
|
|
room.updateMyMembership(KnownMembership.Join);
|
|
expect(room.maySendMessage()).toEqual(true);
|
|
});
|
|
});
|
|
|
|
describe("getDefaultRoomName", function () {
|
|
it("should return 'Empty room' if a user is the only member", function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
|
});
|
|
|
|
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);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
|
});
|
|
|
|
it("should return a display name if one other member is banned", async function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Ban,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
|
|
});
|
|
|
|
it("should return a display name if one other member is invited", async function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Invite,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
|
});
|
|
|
|
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);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Leave,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
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", async function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userC,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User C",
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
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", async function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userC,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User C",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userD,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User D",
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B and 2 others");
|
|
});
|
|
});
|
|
|
|
describe("io.element.functional_users", 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);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
service_members: [],
|
|
},
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
|
});
|
|
|
|
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);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
service_members: 1,
|
|
},
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
|
});
|
|
|
|
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);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
service_members: userB,
|
|
},
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
|
});
|
|
|
|
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);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
service_members: [userB],
|
|
},
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
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", async function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userC,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User C",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
user: userA,
|
|
content: {
|
|
service_members: [userC],
|
|
},
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
|
});
|
|
|
|
it("should return 'Empty room' if all other members are functional members", async function () {
|
|
const room = new Room(roomId, new TestClient(userA).client, userA);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userC,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User C",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
user: userA,
|
|
content: {
|
|
service_members: [userB, userC],
|
|
},
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
|
});
|
|
|
|
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);
|
|
await room.addLiveEvents(
|
|
[
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User A",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "User B",
|
|
}),
|
|
utils.mkEvent({
|
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
|
skey: "",
|
|
room: roomId,
|
|
event: true,
|
|
user: userA,
|
|
content: {
|
|
service_members: [userC],
|
|
},
|
|
}),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
|
});
|
|
});
|
|
|
|
describe("threads", function () {
|
|
beforeEach(() => {
|
|
const client = new TestClient("@alice:example.com", "alicedevice").client;
|
|
room = new Room(roomId, client, userA);
|
|
client.getRoom = () => room;
|
|
});
|
|
|
|
it("allow create threads without a root event", function () {
|
|
const eventWithoutARootEvent = new MatrixEvent({
|
|
event_id: "$123",
|
|
room_id: roomId,
|
|
content: {
|
|
"m.relates_to": {
|
|
rel_type: "m.thread",
|
|
event_id: "$000",
|
|
},
|
|
},
|
|
unsigned: {
|
|
age: 1,
|
|
},
|
|
});
|
|
|
|
room.createThread("$000", undefined, [eventWithoutARootEvent], false);
|
|
|
|
const rootEvent = new MatrixEvent({
|
|
event_id: "$666",
|
|
room_id: roomId,
|
|
content: {},
|
|
unsigned: {
|
|
"age": 1,
|
|
"m.relations": {
|
|
"m.thread": {
|
|
latest_event: null,
|
|
count: 1,
|
|
current_user_participated: false,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(() => room.createThread(rootEvent.getId()!, rootEvent, [], false)).not.toThrow();
|
|
});
|
|
|
|
it("returns the same model when creating a thread twice", () => {
|
|
const { thread, rootEvent } = mkThread({ room });
|
|
|
|
expect(thread).toBeInstanceOf(Thread);
|
|
|
|
const duplicateThread = room.createThread(rootEvent.getId()!, rootEvent, [], false);
|
|
|
|
expect(duplicateThread).toBe(thread);
|
|
});
|
|
|
|
it("creating thread from edited event should not conflate old versions of the event", () => {
|
|
const message = mkMessage();
|
|
const edit = mkEdit(message);
|
|
message.makeReplaced(edit);
|
|
|
|
const thread = room.createThread("$000", message, [], true);
|
|
expect(thread).toHaveLength(0);
|
|
});
|
|
|
|
it("Edits update the lastReply event", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
|
|
const randomMessage = mkMessage();
|
|
const threadRoot = mkMessage();
|
|
const threadResponse = mkThreadResponse(threadRoot);
|
|
threadResponse.localTimestamp += 1000;
|
|
const threadResponseEdit = mkEdit(threadResponse);
|
|
threadResponseEdit.localTimestamp += 2000;
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
[THREAD_RELATION_TYPE.name]: {
|
|
latest_event: threadResponse.event,
|
|
count: 2,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
room.client.fetchRelations = (
|
|
roomId: string,
|
|
eventId: string,
|
|
relationType?: RelationType | string | null,
|
|
eventType?: EventType | string | null,
|
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
|
) =>
|
|
Promise.resolve({
|
|
chunk: [threadResponse.event] as IEvent[],
|
|
next_batch: "start_token",
|
|
});
|
|
|
|
const prom = emitPromise(room, ThreadEvent.New);
|
|
await room.addLiveEvents([randomMessage, threadRoot, threadResponse], { addToState: false });
|
|
const thread: Thread = await prom;
|
|
await emitPromise(room, ThreadEvent.Update);
|
|
|
|
expect(thread.initialEventsFetched).toBeTruthy();
|
|
expect(thread.replyToEvent!.event).toEqual(threadResponse.event);
|
|
expect(thread.replyToEvent!.getContent().body).toBe(threadResponse.getContent().body);
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
[THREAD_RELATION_TYPE.name]: {
|
|
latest_event: {
|
|
...threadResponse.event,
|
|
content: threadResponseEdit.getContent()["m.new_content"],
|
|
},
|
|
count: 2,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// XXX: If we add the relation to the thread response before the thread finishes fetching via /relations
|
|
// then the test will fail
|
|
await emitPromise(room, ThreadEvent.Update);
|
|
await Promise.all([
|
|
emitPromise(room, ThreadEvent.Update),
|
|
room.addLiveEvents([threadResponseEdit], { addToState: false }),
|
|
]);
|
|
expect(thread.replyToEvent!.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
|
|
});
|
|
|
|
it("emits event for the first event added to a thread", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
|
|
await room.addLiveEvents([threadRoot], { addToState: false });
|
|
|
|
const onEvent = jest.fn();
|
|
room.on(RoomEvent.Timeline, onEvent);
|
|
|
|
await room.addLiveEvents([threadResponse1], { addToState: false });
|
|
|
|
expect(onEvent).toHaveBeenCalled();
|
|
});
|
|
|
|
it("contains the events added as soon as it's created", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
|
|
const newThreadEventPromise = emitPromise(room, ThreadEvent.New);
|
|
await room.addLiveEvents([threadRoot, threadResponse1], { addToState: false });
|
|
const thread = await newThreadEventPromise;
|
|
|
|
expect(thread.timeline).toContain(threadResponse1);
|
|
});
|
|
|
|
it("Redactions to thread responses decrement the length", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
threadResponse1.localTimestamp += 1000;
|
|
const threadResponse2 = mkThreadResponse(threadRoot);
|
|
threadResponse2.localTimestamp += 2000;
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
[THREAD_RELATION_TYPE.name]: {
|
|
latest_event: threadResponse2.event,
|
|
count: 2,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
let prom = emitPromise(room, ThreadEvent.New);
|
|
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2], { addToState: false });
|
|
const thread = await prom;
|
|
|
|
expect(thread).toHaveLength(2);
|
|
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
|
|
|
thread.timelineSet.addEventToTimeline(threadResponse1, thread.liveTimeline, {
|
|
toStartOfTimeline: true,
|
|
fromCache: false,
|
|
roomState: thread.roomState,
|
|
});
|
|
thread.timelineSet.addEventToTimeline(threadResponse2, thread.liveTimeline, {
|
|
toStartOfTimeline: true,
|
|
fromCache: false,
|
|
roomState: thread.roomState,
|
|
});
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
[THREAD_RELATION_TYPE.name]: {
|
|
latest_event: threadResponse2.event,
|
|
count: 1,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
prom = emitPromise(thread, ThreadEvent.Update);
|
|
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
|
await room.addLiveEvents([threadResponse1Redaction], { addToState: false });
|
|
await prom;
|
|
expect(thread).toHaveLength(1);
|
|
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
|
});
|
|
|
|
it("Redactions to reactions in threads do not decrement the length", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
threadResponse1.localTimestamp += 1000;
|
|
const threadResponse2 = mkThreadResponse(threadRoot);
|
|
threadResponse2.localTimestamp += 2000;
|
|
const threadResponse2Reaction = utils.mkReaction(threadResponse2, room.client, userA, roomId);
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
[THREAD_RELATION_TYPE.name]: {
|
|
latest_event: threadResponse2.event,
|
|
count: 2,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
room.client.fetchRelations = jest.fn().mockResolvedValue({
|
|
chunk: [threadResponse2Reaction.event, threadResponse2.event, threadResponse1.event],
|
|
});
|
|
|
|
const prom = emitPromise(room, ThreadEvent.New);
|
|
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction], {
|
|
addToState: false,
|
|
});
|
|
const thread = await prom;
|
|
await emitPromise(room, ThreadEvent.Update);
|
|
|
|
expect(thread).toHaveLength(2);
|
|
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
|
|
|
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
|
|
await room.addLiveEvents([threadResponse2ReactionRedaction], { addToState: false });
|
|
expect(thread).toHaveLength(2);
|
|
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
|
});
|
|
|
|
it("should not decrement the length when the thread root is redacted", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
threadResponse1.localTimestamp += 1000;
|
|
const threadResponse2 = mkThreadResponse(threadRoot);
|
|
threadResponse2.localTimestamp += 2000;
|
|
const threadResponse2Reaction = utils.mkReaction(threadResponse2, room.client, userA, roomId);
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
[THREAD_RELATION_TYPE.name]: {
|
|
latest_event: threadResponse2.event,
|
|
count: 2,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const prom = emitPromise(room, ThreadEvent.New);
|
|
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction], {
|
|
addToState: false,
|
|
});
|
|
const thread = await prom;
|
|
|
|
expect(thread).toHaveLength(2);
|
|
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
|
|
|
const threadRootRedaction = mkRedaction(threadRoot);
|
|
await room.addLiveEvents([threadRootRedaction], { addToState: false });
|
|
|
|
// We can't wait for a thread update here because there shouldn't be one (which is
|
|
// what we're asserting). Flush any promises to try to get more certainty that an
|
|
// update is not happening some time after the event is added.
|
|
await flushPromises();
|
|
expect(thread).toHaveLength(2);
|
|
});
|
|
|
|
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
|
|
|
room.client.createThreadListMessagesRequest = () =>
|
|
Promise.resolve({
|
|
chunk: [],
|
|
state: [],
|
|
});
|
|
|
|
await room.createThreadsTimelineSets();
|
|
await room.fetchRoomThreads();
|
|
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
threadResponse1.localTimestamp += 1000;
|
|
const threadResponse2 = mkThreadResponse(threadRoot);
|
|
threadResponse2.localTimestamp += 2000;
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
"m.thread": {
|
|
latest_event: threadResponse2.event,
|
|
count: 1,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
room.client.fetchRelations = (
|
|
roomId: string,
|
|
eventId: string,
|
|
relationType?: RelationType | string | null,
|
|
eventType?: EventType | string | null,
|
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
|
) =>
|
|
Promise.resolve({
|
|
chunk: [threadResponse1.event] as IEvent[],
|
|
next_batch: "start_token",
|
|
});
|
|
|
|
let prom = emitPromise(room, ThreadEvent.New);
|
|
await room.addLiveEvents([threadRoot, threadResponse1], { addToState: false });
|
|
const thread: Thread = await prom;
|
|
await emitPromise(room, ThreadEvent.Update);
|
|
|
|
expect(thread.initialEventsFetched).toBeTruthy();
|
|
await room.addLiveEvents([threadResponse2], { addToState: false });
|
|
expect(thread).toHaveLength(2);
|
|
expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId());
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
"m.thread": {
|
|
latest_event: threadResponse1.event,
|
|
count: 1,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await emitPromise(room, ThreadEvent.Update);
|
|
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
|
await room.addLiveEvents([threadResponse2Redaction], { addToState: false });
|
|
expect(thread).toHaveLength(1);
|
|
expect(thread.replyToEvent!.getId()).toBe(threadResponse1.getId());
|
|
|
|
room.client.fetchRoomEvent = (eventId: string) =>
|
|
Promise.resolve({
|
|
...threadRoot.event,
|
|
unsigned: {
|
|
"age": 123,
|
|
"m.relations": {
|
|
"m.thread": {
|
|
latest_event: threadRoot.event,
|
|
count: 0,
|
|
current_user_participated: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
prom = emitPromise(room, ThreadEvent.Delete);
|
|
const prom2 = emitPromise(room, RoomEvent.Timeline);
|
|
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
|
await room.addLiveEvents([threadResponse1Redaction], { addToState: false });
|
|
await prom;
|
|
await prom2;
|
|
expect(thread).toHaveLength(0);
|
|
expect(thread.replyToEvent!.getId()).toBe(threadRoot.getId());
|
|
});
|
|
|
|
it("should add event to thread without server side support", async () => {
|
|
room.client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.None);
|
|
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
threadResponse1.getContent()["m.relates_to"]!.rel_type = "io.element.thread";
|
|
|
|
const thread = room.createThread(threadRoot.getId()!, threadRoot, [threadResponse1], false)!;
|
|
|
|
expect(thread.events).toContain(threadResponse1);
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Clear the latch created by `Thread.setServerSideSupport(FeatureSupport.None);`
|
|
FILTER_RELATED_BY_SENDERS.setPreferUnstable(false);
|
|
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(false);
|
|
THREAD_RELATION_TYPE.setPreferUnstable(false);
|
|
});
|
|
});
|
|
|
|
describe("eventShouldLiveIn", () => {
|
|
const client = new TestClient(userA).client;
|
|
const room = new Room(roomId, client, userA);
|
|
|
|
beforeEach(() => {
|
|
client.supportsThreads = () => true;
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
});
|
|
|
|
it("thread root and its relations&redactions should be in main timeline", () => {
|
|
const randomMessage = mkMessage();
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
const threadReaction1 = utils.mkReaction(threadRoot, room.client, userA, roomId);
|
|
const threadReaction2 = utils.mkReaction(threadRoot, room.client, userA, roomId);
|
|
const threadReaction2Redaction = mkRedaction(threadReaction2);
|
|
|
|
const roots = new Set([threadRoot.getId()!]);
|
|
const events = [
|
|
randomMessage,
|
|
threadRoot,
|
|
threadResponse1,
|
|
threadReaction1,
|
|
threadReaction2,
|
|
threadReaction2Redaction,
|
|
];
|
|
|
|
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
|
|
events.slice(1).forEach((ev) => ev.setThread(thread));
|
|
|
|
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInThread).toBeFalsy();
|
|
|
|
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadRoot, events, roots).threadId).toBe(threadRoot.getId());
|
|
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
|
|
|
|
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
|
|
});
|
|
|
|
it("thread response and its relations&redactions should be only in thread timeline", () => {
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
const threadReaction1 = utils.mkReaction(threadResponse1, room.client, userA, roomId);
|
|
const threadReaction2 = utils.mkReaction(threadResponse1, room.client, userA, roomId);
|
|
const threadReaction2Redaction = mkRedaction(threadReaction2);
|
|
|
|
const roots = new Set([threadRoot.getId()!]);
|
|
const events = [threadRoot, threadResponse1, threadReaction1, threadReaction2, threadReaction2Redaction];
|
|
|
|
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadReaction1, events, roots).threadId).toBe(threadRoot.getId());
|
|
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadReaction2, events, roots).threadId).toBe(threadRoot.getId());
|
|
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
|
|
});
|
|
|
|
it("reply to thread response and its relations&redactions should be only in thread timeline", () => {
|
|
const threadRoot = mkMessage();
|
|
const threadResp1 = mkThreadResponse(threadRoot);
|
|
const threadResp1Reply1 = mkReply(threadResp1);
|
|
const threadResp1Reply1Reaction1 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
|
|
const threadResp1Reply1Reaction2 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
|
|
const thResp1Rep1React2Redaction = mkRedaction(threadResp1Reply1);
|
|
|
|
const roots = new Set([threadRoot.getId()!]);
|
|
const events = [
|
|
threadRoot,
|
|
threadResp1,
|
|
threadResp1Reply1,
|
|
threadResp1Reply1Reaction1,
|
|
threadResp1Reply1Reaction2,
|
|
thResp1Rep1React2Redaction,
|
|
];
|
|
|
|
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
|
|
events.forEach((ev) => ev.setThread(thread));
|
|
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).threadId).toBe(thread.id);
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).threadId).toBe(thread.id);
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).threadId).toBe(thread.id);
|
|
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).threadId).toBe(thread.id);
|
|
});
|
|
|
|
it("reply to reply to thread root should only be in the main timeline", () => {
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
const reply1 = mkReply(threadRoot);
|
|
const reply2 = mkReply(reply1);
|
|
|
|
const roots = new Set([threadRoot.getId()!]);
|
|
const events = [threadRoot, threadResponse1, reply1, reply2];
|
|
|
|
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
|
|
threadResponse1.setThread(thread);
|
|
|
|
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy();
|
|
});
|
|
|
|
it("edit to thread root should live in main timeline only", () => {
|
|
const threadRoot = mkMessage();
|
|
const threadResponse1 = mkThreadResponse(threadRoot);
|
|
const threadRootEdit = mkEdit(threadRoot);
|
|
threadRoot.makeReplaced(threadRootEdit);
|
|
|
|
const thread = room.createThread(threadRoot.getId()!, threadRoot, [threadResponse1], false);
|
|
threadResponse1.setThread(thread);
|
|
threadRootEdit.setThread(thread);
|
|
|
|
const roots = new Set([threadRoot.getId()!]);
|
|
const events = [threadRoot, threadResponse1, threadRootEdit];
|
|
|
|
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadRoot, events, roots).threadId).toBe(threadRoot.getId());
|
|
|
|
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInRoom).toBeFalsy();
|
|
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInThread).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
|
|
|
|
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInThread).toBeFalsy();
|
|
});
|
|
|
|
it("should aggregate relations in thread event timeline set", async () => {
|
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
|
const threadRoot = mkMessage();
|
|
const rootReaction = utils.mkReaction(threadRoot, room.client, userA, roomId);
|
|
const threadResponse = mkThreadResponse(threadRoot);
|
|
const threadReaction = utils.mkReaction(threadResponse, room.client, userA, roomId);
|
|
|
|
const events = [threadRoot, rootReaction, threadResponse, threadReaction];
|
|
|
|
const prom = emitPromise(room, ThreadEvent.New);
|
|
await room.addLiveEvents(events, { addToState: false });
|
|
const thread = await prom;
|
|
expect(thread).toBe(threadRoot.getThread());
|
|
expect(thread.rootEvent).toBe(threadRoot);
|
|
|
|
const rootRelations = thread.timelineSet.relations
|
|
.getChildEventsForEvent(threadRoot.getId()!, RelationType.Annotation, EventType.Reaction)!
|
|
.getSortedAnnotationsByKey();
|
|
expect(rootRelations).toHaveLength(1);
|
|
expect(rootRelations![0][0]).toEqual(rootReaction.getRelation()!.key);
|
|
expect(rootRelations![0][1].size).toEqual(1);
|
|
expect(rootRelations![0][1].has(rootReaction)).toBeTruthy();
|
|
|
|
const responseRelations = thread.timelineSet.relations
|
|
.getChildEventsForEvent(threadResponse.getId()!, RelationType.Annotation, EventType.Reaction)!
|
|
.getSortedAnnotationsByKey();
|
|
expect(responseRelations).toHaveLength(1);
|
|
expect(responseRelations![0][0]).toEqual(threadReaction.getRelation()!.key);
|
|
expect(responseRelations![0][1].size).toEqual(1);
|
|
expect(responseRelations![0][1].has(threadReaction)).toBeTruthy();
|
|
});
|
|
|
|
it("a non-thread reply to an unknown parent event should live in the main timeline only", async () => {
|
|
const message = mkMessage(); // we do not add this message to any timelines
|
|
const reply = mkReply(message);
|
|
|
|
expect(room.eventShouldLiveIn(reply).shouldLiveInRoom).toBeTruthy();
|
|
expect(room.eventShouldLiveIn(reply).shouldLiveInThread).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe("getEventReadUpTo()", () => {
|
|
const client = new TestClient(userA).client;
|
|
const room = new Room(roomId, client, userA);
|
|
|
|
describe("invalid receipts", () => {
|
|
beforeEach(() => {
|
|
// Clear the spies on logger.warn
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it("ignores receipts pointing at missing events", () => {
|
|
// Given a receipt exists
|
|
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
|
|
return { eventId: "missingEventId" } as WrappedReceipt;
|
|
};
|
|
// But the event ID it contains does not refer to an event we have
|
|
room.findEventById = jest.fn().mockReturnValue(null);
|
|
|
|
// When we ask what they have read
|
|
// Then we say "nothing"
|
|
expect(room.getEventReadUpTo(userA)).toBeNull();
|
|
});
|
|
|
|
it("ignores receipts pointing at the wrong thread", () => {
|
|
// Given a threaded receipt exists
|
|
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
|
|
return { eventId: "wrongThreadEventId", data: { ts: 0, thread_id: "thread1" } } as WrappedReceipt;
|
|
};
|
|
// But the event it refers to is in a thread
|
|
room.findEventById = jest.fn().mockReturnValue({ threadRootId: "thread2" } as MatrixEvent);
|
|
|
|
// When we ask what they have read
|
|
// Then we say "nothing"
|
|
expect(room.getEventReadUpTo(userA)).toBeNull();
|
|
expect(logger.warn).toHaveBeenCalledWith(
|
|
"Ignoring receipt because its thread_id (thread1) disagrees with the thread root (thread2) " +
|
|
"of the referenced event (event ID = wrongThreadEventId)",
|
|
);
|
|
});
|
|
|
|
it("accepts unthreaded receipts pointing at an event in a thread", () => {
|
|
// Given an unthreaded receipt exists
|
|
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
|
|
return { eventId: "inThreadEventId" } as WrappedReceipt;
|
|
};
|
|
// And the event it refers to is in a thread
|
|
room.findEventById = jest.fn().mockReturnValue({ threadRootId: "thread2" } as MatrixEvent);
|
|
|
|
// When we ask what they have read
|
|
// Then we say the event
|
|
expect(room.getEventReadUpTo(userA)).toEqual("inThreadEventId");
|
|
});
|
|
|
|
it("accepts main thread receipts pointing at an event in main timeline", () => {
|
|
// Given a threaded receipt exists, in main thread
|
|
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
|
|
return { eventId: "mainThreadEventId", data: { ts: 12, thread_id: "main" } } as WrappedReceipt;
|
|
};
|
|
// And the event it refers to is in a thread
|
|
room.findEventById = jest.fn().mockReturnValue({ threadRootId: undefined } as MatrixEvent);
|
|
|
|
// When we ask what they have read
|
|
// Then we say the event
|
|
expect(room.getEventReadUpTo(userA)).toEqual("mainThreadEventId");
|
|
});
|
|
|
|
it("accepts main thread receipts pointing at a thread root", () => {
|
|
// Given a threaded receipt exists, in main thread
|
|
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
|
|
return { eventId: "rootId", data: { ts: 12, thread_id: "main" } } as WrappedReceipt;
|
|
};
|
|
// And the event it refers to is in a thread, because it is a thread root
|
|
room.findEventById = jest
|
|
.fn()
|
|
.mockReturnValue({ isThreadRoot: true, threadRootId: "thread1" } as MatrixEvent);
|
|
|
|
// When we ask what they have read
|
|
// Then we say the event
|
|
expect(room.getEventReadUpTo(userA)).toEqual("rootId");
|
|
});
|
|
});
|
|
|
|
describe("valid receipts", () => {
|
|
beforeEach(() => {
|
|
// When we look up the event referred to by the receipt, it exists
|
|
room.findEventById = jest.fn().mockReturnValue({} as MatrixEvent);
|
|
});
|
|
|
|
it("handles missing receipt type", () => {
|
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
|
return receiptType === ReceiptType.ReadPrivate ? ({ eventId: "eventId" } as WrappedReceipt) : null;
|
|
};
|
|
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
|
|
});
|
|
|
|
describe("prefers newer receipt", () => {
|
|
it("should compare correctly using timelines", () => {
|
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
|
if (receiptType === ReceiptType.ReadPrivate) {
|
|
return { eventId: "eventId1" } as WrappedReceipt;
|
|
}
|
|
if (receiptType === ReceiptType.Read) {
|
|
return { eventId: "eventId2" } as WrappedReceipt;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
for (let i = 1; i <= 2; i++) {
|
|
room.getUnfilteredTimelineSet = () =>
|
|
({
|
|
compareEventOrdering: (event1: string, _event2: string) => {
|
|
return event1 === `eventId${i}` ? 1 : -1;
|
|
},
|
|
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
|
|
}) as unknown as EventTimelineSet;
|
|
|
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
|
|
}
|
|
});
|
|
|
|
describe("correctly compares by timestamp", () => {
|
|
it("should correctly compare, if we have all receipts", () => {
|
|
for (let i = 1; i <= 2; i++) {
|
|
room.getUnfilteredTimelineSet = () =>
|
|
({
|
|
compareEventOrdering: () => null,
|
|
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
|
|
}) as unknown as EventTimelineSet;
|
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
|
if (receiptType === ReceiptType.ReadPrivate) {
|
|
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
|
|
}
|
|
if (receiptType === ReceiptType.Read) {
|
|
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
|
|
}
|
|
});
|
|
|
|
it("should correctly compare, if private read receipt is missing", () => {
|
|
room.getUnfilteredTimelineSet = () =>
|
|
({
|
|
compareEventOrdering: () => null,
|
|
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
|
|
}) as unknown as EventTimelineSet;
|
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
|
if (receiptType === ReceiptType.Read) {
|
|
return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
|
|
});
|
|
});
|
|
|
|
describe("fallback precedence", () => {
|
|
beforeAll(() => {
|
|
room.getUnfilteredTimelineSet = () =>
|
|
({
|
|
compareEventOrdering: () => null,
|
|
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
|
|
}) as unknown as EventTimelineSet;
|
|
});
|
|
|
|
it("should give precedence to m.read.private", () => {
|
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
|
if (receiptType === ReceiptType.ReadPrivate) {
|
|
return { eventId: "eventId1", data: { ts: 123 } };
|
|
}
|
|
if (receiptType === ReceiptType.Read) {
|
|
return { eventId: "eventId2", data: { ts: 123 } };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`);
|
|
});
|
|
|
|
it("should give precedence to m.read", () => {
|
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
|
if (receiptType === ReceiptType.Read) {
|
|
return { eventId: "eventId3" } as WrappedReceipt;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("roomNameGenerator", () => {
|
|
const client = new TestClient(userA).client;
|
|
client.roomNameGenerator = jest.fn().mockReturnValue(null);
|
|
const room = new Room(roomId, client, userA);
|
|
|
|
it("should call fn when recalculating room name", () => {
|
|
(client.roomNameGenerator as jest.Mock).mockClear();
|
|
room.recalculate();
|
|
expect(client.roomNameGenerator).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("thread notifications", () => {
|
|
let room: Room;
|
|
|
|
beforeEach(() => {
|
|
const client = new TestClient(userA).client;
|
|
room = new Room(roomId, client, userA);
|
|
});
|
|
|
|
it("defaults to undefined", () => {
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0);
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
|
|
});
|
|
|
|
it("lets you set values", () => {
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
|
|
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
|
|
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10);
|
|
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(10);
|
|
});
|
|
|
|
it("lets you reset threads notifications", () => {
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
|
|
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
|
|
|
room.resetThreadUnreadNotificationCountFromSync();
|
|
|
|
expect(room.threadsAggregateNotificationType).toBe(null);
|
|
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0);
|
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
|
|
});
|
|
|
|
it("sets the room threads notification type", () => {
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 333);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
|
});
|
|
|
|
it("emits event on notifications reset", () => {
|
|
const cb = jest.fn();
|
|
|
|
room.on(RoomEvent.UnreadNotifications, cb);
|
|
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
|
room.setThreadUnreadNotificationCount("456", NotificationCountType.Highlight, 123);
|
|
|
|
room.resetThreadUnreadNotificationCountFromSync();
|
|
|
|
expect(cb).toHaveBeenLastCalledWith();
|
|
});
|
|
});
|
|
|
|
describe("hasThreadUnreadNotification", () => {
|
|
it("has no notifications by default", () => {
|
|
expect(room.hasThreadUnreadNotification()).toBe(false);
|
|
});
|
|
|
|
it("main timeline notification does not affect this", () => {
|
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
|
expect(room.hasThreadUnreadNotification()).toBe(false);
|
|
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
|
expect(room.hasThreadUnreadNotification()).toBe(false);
|
|
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
|
|
expect(room.hasThreadUnreadNotification()).toBe(true);
|
|
});
|
|
|
|
it("lets you reset", () => {
|
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
|
|
expect(room.hasThreadUnreadNotification()).toBe(true);
|
|
|
|
room.resetThreadUnreadNotificationCountFromSync();
|
|
|
|
expect(room.hasThreadUnreadNotification()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("threadsAggregateNotificationType", () => {
|
|
it("defaults to null", () => {
|
|
expect(room.threadsAggregateNotificationType).toBeNull();
|
|
});
|
|
|
|
it("counts multiple threads", () => {
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
|
|
|
|
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
|
|
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
|
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 0);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
|
|
});
|
|
|
|
it("allows reset", () => {
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
|
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
|
|
|
|
room.resetThreadUnreadNotificationCountFromSync();
|
|
|
|
expect(room.threadsAggregateNotificationType).toBeNull();
|
|
});
|
|
|
|
it("retains highlight for encrypted rooms on reset", () => {
|
|
room.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
|
|
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 2);
|
|
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
|
|
|
room.resetThreadUnreadNotificationCountFromSync();
|
|
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
|
});
|
|
|
|
it("resets highlight for unencrypted rooms on reset", () => {
|
|
room.hasEncryptionStateEvent = jest.fn().mockReturnValue(false);
|
|
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 2);
|
|
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
|
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
|
|
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
|
|
|
room.resetThreadUnreadNotificationCountFromSync();
|
|
|
|
expect(room.threadsAggregateNotificationType).toBe(null);
|
|
expect(room.getThreadUnreadNotificationCount("$123", NotificationCountType.Highlight)).toBe(0);
|
|
});
|
|
});
|
|
|
|
it("should load pending events from from the store and decrypt if needed", async () => {
|
|
const client = new TestClient(userA).client;
|
|
client["cryptoBackend"] = {
|
|
decryptEvent: jest.fn().mockResolvedValue({ clearEvent: { body: "enc" } }),
|
|
} as unknown as CryptoBackend;
|
|
client.store.getPendingEvents = jest.fn(async (roomId) => [
|
|
{
|
|
event_id: "$1:server",
|
|
type: "m.room.message",
|
|
content: { body: "1" },
|
|
sender: "@1:server",
|
|
room_id: roomId,
|
|
origin_server_ts: 1,
|
|
txn_id: "txn1",
|
|
},
|
|
{
|
|
event_id: "$2:server",
|
|
type: "m.room.encrypted",
|
|
content: { body: "2" },
|
|
sender: "@2:server",
|
|
room_id: roomId,
|
|
origin_server_ts: 2,
|
|
txn_id: "txn2",
|
|
},
|
|
]);
|
|
const room = new Room(roomId, client, userA, {
|
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
});
|
|
await emitPromise(room, RoomEvent.LocalEchoUpdated);
|
|
await emitPromise(client, MatrixEventEvent.Decrypted);
|
|
await emitPromise(room, RoomEvent.LocalEchoUpdated);
|
|
const pendingEvents = room.getPendingEvents();
|
|
expect(pendingEvents).toHaveLength(2);
|
|
expect(pendingEvents[1].isDecryptionFailure()).toBeFalsy();
|
|
expect(pendingEvents[1].isBeingDecrypted()).toBeFalsy();
|
|
expect(pendingEvents[1].isEncrypted()).toBeTruthy();
|
|
for (const ev of pendingEvents) {
|
|
expect(room.getPendingEvent(ev.getId()!)).toBe(ev);
|
|
}
|
|
});
|
|
|
|
describe("getBlacklistUnverifiedDevices", () => {
|
|
it("defaults to null", () => {
|
|
expect(room.getBlacklistUnverifiedDevices()).toBeNull();
|
|
});
|
|
|
|
it("is updated by setBlacklistUnverifiedDevices", () => {
|
|
room.setBlacklistUnverifiedDevices(false);
|
|
expect(room.getBlacklistUnverifiedDevices()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("processPollEvents()", () => {
|
|
let room: Room;
|
|
let client: MatrixClient;
|
|
|
|
beforeEach(() => {
|
|
client = getMockClientWithEventEmitter({
|
|
decryptEventIfNeeded: jest.fn(),
|
|
});
|
|
room = new Room(roomId, client, userA);
|
|
jest.spyOn(room, "emit").mockClear();
|
|
});
|
|
|
|
const makePollStart = (id: string): MatrixEvent => {
|
|
const event = new MatrixEvent({
|
|
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
|
room_id: roomId,
|
|
});
|
|
event.event.event_id = id;
|
|
return event;
|
|
};
|
|
|
|
it("adds poll models to room state for a poll start event", async () => {
|
|
const pollStartEvent = makePollStart("1");
|
|
const events = [pollStartEvent];
|
|
|
|
await room.processPollEvents(events);
|
|
expect(client.decryptEventIfNeeded).toHaveBeenCalledWith(pollStartEvent);
|
|
const pollInstance = room.polls.get(pollStartEvent.getId()!);
|
|
expect(pollInstance).toBeTruthy();
|
|
|
|
expect(room.emit).toHaveBeenCalledWith(PollEvent.New, pollInstance);
|
|
});
|
|
|
|
it("adds related events to poll models and log errors", async () => {
|
|
const pollStartEvent = makePollStart("1");
|
|
const pollStartEvent2 = makePollStart("2");
|
|
const events = [pollStartEvent, pollStartEvent2];
|
|
const pollResponseEvent = new MatrixEvent({
|
|
type: M_POLL_RESPONSE.name,
|
|
content: {
|
|
"m.relates_to": {
|
|
rel_type: RelationType.Reference,
|
|
event_id: pollStartEvent.getId(),
|
|
},
|
|
},
|
|
});
|
|
|
|
const messageEvent = new MatrixEvent({
|
|
type: "m.room.messsage",
|
|
content: {
|
|
text: "hello",
|
|
},
|
|
});
|
|
|
|
const errorEvent = new MatrixEvent({
|
|
type: M_POLL_START.name,
|
|
content: {
|
|
text: "Error!!!!",
|
|
},
|
|
});
|
|
|
|
const error = new Error("Test error");
|
|
|
|
mocked(client.decryptEventIfNeeded).mockImplementation(async (event: MatrixEvent) => {
|
|
if (event === errorEvent) throw error;
|
|
});
|
|
|
|
// init poll
|
|
await room.processPollEvents(events);
|
|
|
|
const poll = room.polls.get(pollStartEvent.getId()!)!;
|
|
const poll2 = room.polls.get(pollStartEvent2.getId()!)!;
|
|
jest.spyOn(poll, "onNewRelation");
|
|
jest.spyOn(poll2, "onNewRelation");
|
|
|
|
await room.processPollEvents([errorEvent, messageEvent, pollResponseEvent]);
|
|
|
|
// only called for relevant event
|
|
expect(poll.onNewRelation).toHaveBeenCalledTimes(1);
|
|
expect(poll.onNewRelation).toHaveBeenCalledWith(pollResponseEvent);
|
|
|
|
// only called on poll with relation
|
|
expect(poll2.onNewRelation).not.toHaveBeenCalled();
|
|
|
|
expect(logger.warn).toHaveBeenCalledWith("Error processing poll event", errorEvent.getId(), error);
|
|
});
|
|
|
|
it("should retry on decryption", async () => {
|
|
const pollStartEventId = "poll1";
|
|
const pollStartEvent = makePollStart(pollStartEventId);
|
|
// simulate decryption failure
|
|
const isDecryptionFailureSpy = jest.spyOn(pollStartEvent, "isDecryptionFailure").mockReturnValue(true);
|
|
|
|
await room.processPollEvents([pollStartEvent]);
|
|
// do not expect a poll to show up for the room
|
|
expect(room.polls.get(pollStartEventId)).toBeUndefined();
|
|
|
|
// now emit a Decrypted event but keep the decryption failure
|
|
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent);
|
|
// still do not expect a poll to show up for the room
|
|
expect(room.polls.get(pollStartEventId)).toBeUndefined();
|
|
|
|
// clear decryption failure and emit a Decrypted event again
|
|
isDecryptionFailureSpy.mockRestore();
|
|
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent);
|
|
|
|
// the poll should now show up in the room's polls
|
|
const poll = room.polls.get(pollStartEventId);
|
|
expect(poll?.pollId).toBe(pollStartEventId);
|
|
});
|
|
|
|
it("removes poll from state when redacted", async () => {
|
|
const pollStartEvent = makePollStart("1");
|
|
const events = [pollStartEvent];
|
|
|
|
await room.processPollEvents(events);
|
|
|
|
expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy();
|
|
|
|
const redactedEvent = new MatrixEvent({ type: "m.room.redaction" });
|
|
pollStartEvent.makeRedacted(redactedEvent, room);
|
|
|
|
await flushPromises();
|
|
|
|
// removed from poll state
|
|
expect(room.polls.get(pollStartEvent.getId()!)).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe("findPredecessorRoomId", () => {
|
|
let client: MatrixClient | null = null;
|
|
beforeEach(() => {
|
|
client = getMockClientWithEventEmitter({
|
|
...mockClientMethodsUser(),
|
|
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
|
supportsThreads: jest.fn().mockReturnValue(true),
|
|
});
|
|
});
|
|
|
|
function roomCreateEvent(newRoomId: string, predecessorRoomId: string | null): MatrixEvent {
|
|
const content: {
|
|
["m.federate"]: boolean;
|
|
room_version: string;
|
|
predecessor: { event_id: string; room_id: string } | undefined;
|
|
} = {
|
|
"predecessor": undefined,
|
|
"m.federate": true,
|
|
"room_version": "9",
|
|
};
|
|
if (predecessorRoomId) {
|
|
content.predecessor = {
|
|
event_id: "id_of_last_known_event",
|
|
room_id: predecessorRoomId,
|
|
};
|
|
}
|
|
return new MatrixEvent({
|
|
content,
|
|
event_id: `create_event_id_pred_${predecessorRoomId}`,
|
|
origin_server_ts: 1432735824653,
|
|
room_id: newRoomId,
|
|
sender: "@daryl:alexandria.example.com",
|
|
state_key: "",
|
|
type: "m.room.create",
|
|
});
|
|
}
|
|
|
|
function predecessorEvent(
|
|
newRoomId: string,
|
|
predecessorRoomId: string,
|
|
tombstoneEventId: string | null = null,
|
|
viaServers: string[] = [],
|
|
): MatrixEvent {
|
|
const content =
|
|
tombstoneEventId === null
|
|
? { predecessor_room_id: predecessorRoomId, via_servers: viaServers }
|
|
: {
|
|
predecessor_room_id: predecessorRoomId,
|
|
last_known_event_id: tombstoneEventId,
|
|
via_servers: viaServers,
|
|
};
|
|
|
|
return new MatrixEvent({
|
|
content,
|
|
event_id: `predecessor_event_id_pred_${predecessorRoomId}`,
|
|
origin_server_ts: 1432735824653,
|
|
room_id: newRoomId,
|
|
sender: "@daryl:alexandria.example.com",
|
|
state_key: "",
|
|
type: "org.matrix.msc3946.room_predecessor",
|
|
});
|
|
}
|
|
|
|
it("Returns null if there is no create event", () => {
|
|
const room = new Room("roomid", client!, "@u:example.com");
|
|
expect(room.findPredecessor()).toBeNull();
|
|
});
|
|
|
|
it("Returns null if the create event has no predecessor", async () => {
|
|
const room = new Room("roomid", client!, "@u:example.com");
|
|
await room.addLiveEvents([roomCreateEvent("roomid", null)], { addToState: true });
|
|
expect(room.findPredecessor()).toBeNull();
|
|
});
|
|
|
|
it("Returns the predecessor ID if one is provided via create event", async () => {
|
|
const room = new Room("roomid", client!, "@u:example.com");
|
|
await room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")], { addToState: true });
|
|
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
|
|
});
|
|
|
|
it("Prefers the m.predecessor event if one exists", async () => {
|
|
const room = new Room("roomid", client!, "@u:example.com");
|
|
await room.addLiveEvents(
|
|
[roomCreateEvent("roomid", "replacedroomid"), predecessorEvent("roomid", "otherreplacedroomid")],
|
|
{ addToState: true },
|
|
);
|
|
const useMsc3946 = true;
|
|
expect(room.findPredecessor(useMsc3946)).toEqual({
|
|
roomId: "otherreplacedroomid",
|
|
eventId: undefined, // m.predecessor did not include an event_id
|
|
viaServers: [],
|
|
});
|
|
});
|
|
|
|
it("uses the m.predecessor event ID if provided", async () => {
|
|
const room = new Room("roomid", client!, "@u:example.com");
|
|
await room.addLiveEvents(
|
|
[
|
|
roomCreateEvent("roomid", "replacedroomid"),
|
|
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid", [
|
|
"one.example.com",
|
|
"two.example.com",
|
|
]),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
const useMsc3946 = true;
|
|
expect(room.findPredecessor(useMsc3946)).toEqual({
|
|
roomId: "otherreplacedroomid",
|
|
eventId: "lstevtid",
|
|
viaServers: ["one.example.com", "two.example.com"],
|
|
});
|
|
});
|
|
|
|
it("Ignores the m.predecessor event if we don't ask to use it", async () => {
|
|
const room = new Room("roomid", client!, "@u:example.com");
|
|
await room.addLiveEvents(
|
|
[roomCreateEvent("roomid", "replacedroomid"), predecessorEvent("roomid", "otherreplacedroomid")],
|
|
{ addToState: true },
|
|
);
|
|
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
|
|
// we should ignore the predecessor 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", async () => {
|
|
const room = new Room("roomid", client!, "@u:example.com");
|
|
await room.addLiveEvents(
|
|
[
|
|
roomCreateEvent("roomid", null), // Create event has no predecessor
|
|
predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"),
|
|
],
|
|
{ addToState: true },
|
|
);
|
|
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
|
|
// we should ignore the predecessor event.
|
|
expect(room.findPredecessor()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getLastLiveEvent", () => {
|
|
it("when there are no events, it should return undefined", () => {
|
|
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", async () => {
|
|
const lastEventInMainTimeline = await mkMessageInRoom(room, 23);
|
|
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
|
|
});
|
|
|
|
/**
|
|
* This should normally not happen. The test exists only for the sake of completeness.
|
|
* No event is added to the room's live timeline here.
|
|
*/
|
|
it("when there is no event in the room live timeline but in a thread, it should return the last event from the thread", () => {
|
|
const { thread } = mkThread({ room, length: 0 });
|
|
const lastEventInThread = mkMessageInThread(thread, 42);
|
|
expect(room.getLastLiveEvent()).toBe(lastEventInThread);
|
|
});
|
|
|
|
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", 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", 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", 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", async () => {
|
|
const lastEventInMainTimeline = await mkMessageInRoom(room, 23);
|
|
mkThread({ room, length: 0 });
|
|
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getLastThread", () => {
|
|
it("when there is no thread, it should return undefined", () => {
|
|
expect(room.getLastThread()).toBeUndefined();
|
|
});
|
|
|
|
it("when there is only one thread, it should return this one", () => {
|
|
const { thread1 } = addRoomThreads(room, 23, null);
|
|
expect(room.getLastThread()).toBe(thread1);
|
|
});
|
|
|
|
it("when there are tho threads, it should return the one with the recent event I", () => {
|
|
const { thread2 } = addRoomThreads(room, 23, 42);
|
|
expect(room.getLastThread()).toBe(thread2);
|
|
});
|
|
|
|
it("when there are tho threads, it should return the one with the recent event II", () => {
|
|
const { thread1 } = addRoomThreads(room, 42, 23);
|
|
expect(room.getLastThread()).toBe(thread1);
|
|
});
|
|
|
|
it("when there is a thread with the last event ts undefined, it should return the thread with the defined event ts", () => {
|
|
const { thread2 } = addRoomThreads(room, undefined, 23);
|
|
expect(room.getLastThread()).toBe(thread2);
|
|
});
|
|
|
|
it("when the last event ts of all threads is undefined, it should return the last added thread", () => {
|
|
const { thread2 } = addRoomThreads(room, undefined, undefined);
|
|
expect(room.getLastThread()).toBe(thread2);
|
|
});
|
|
});
|
|
|
|
describe("getRecommendedVersion", () => {
|
|
it("returns the server's recommended version from capabilities", async () => {
|
|
const client = new TestClient(userA).client;
|
|
client.getCapabilities = jest.fn().mockReturnValue({
|
|
["m.room_versions"]: {
|
|
default: "1",
|
|
available: ["1", "2"],
|
|
},
|
|
});
|
|
const room = new Room(roomId, client, userA);
|
|
expect(await room.getRecommendedVersion()).toEqual({
|
|
version: "1",
|
|
needsUpgrade: false,
|
|
urgent: false,
|
|
});
|
|
});
|
|
|
|
it("force-refreshes versions to make sure an upgrade is necessary", async () => {
|
|
const client = new TestClient(userA).client;
|
|
client.getCapabilities = jest.fn().mockReturnValue({
|
|
["m.room_versions"]: {
|
|
default: "5",
|
|
available: ["5"],
|
|
},
|
|
});
|
|
|
|
client.fetchCapabilities = jest.fn().mockResolvedValue({
|
|
["m.room_versions"]: {
|
|
default: "1",
|
|
available: ["1"],
|
|
},
|
|
});
|
|
|
|
const room = new Room(roomId, client, userA);
|
|
expect(await room.getRecommendedVersion()).toEqual({
|
|
version: "1",
|
|
needsUpgrade: false,
|
|
urgent: false,
|
|
});
|
|
expect(client.fetchCapabilities).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("getOrCreateFilteredTimelineSet", () => {
|
|
it("should locally filter events if prepopulateTimeline=true", () => {
|
|
room.addLiveEvents(
|
|
[
|
|
utils.mkEvent(
|
|
{
|
|
event: true,
|
|
type: EventType.RoomMessage,
|
|
user: userA,
|
|
room: roomId,
|
|
content: {
|
|
body: "ev1",
|
|
},
|
|
},
|
|
room.client,
|
|
),
|
|
utils.mkEvent(
|
|
{
|
|
event: true,
|
|
type: "custom.event.type",
|
|
user: userA,
|
|
room: roomId,
|
|
content: {
|
|
body: "ev2",
|
|
},
|
|
},
|
|
room.client,
|
|
),
|
|
utils.mkEvent(
|
|
{
|
|
event: true,
|
|
type: EventType.RoomMessage,
|
|
user: userA,
|
|
room: roomId,
|
|
content: {
|
|
body: "ev3",
|
|
},
|
|
},
|
|
room.client,
|
|
),
|
|
],
|
|
{
|
|
addToState: false,
|
|
},
|
|
);
|
|
|
|
const filter = Filter.fromJson(room.client.getUserId(), "filterId", {
|
|
room: {
|
|
timeline: {
|
|
types: ["custom.event.type"],
|
|
},
|
|
},
|
|
});
|
|
const timelineSet = room.getOrCreateFilteredTimelineSet(filter, { prepopulateTimeline: true });
|
|
const filteredEvents = timelineSet.getLiveTimeline().getEvents();
|
|
expect(filteredEvents).toHaveLength(1);
|
|
expect(filteredEvents[0].getContent().body).toEqual("ev2");
|
|
});
|
|
});
|
|
|
|
it("saves and retrieves the bump stamp", () => {
|
|
room.setBumpStamp(123456789);
|
|
expect(room.getBumpStamp()).toEqual(123456789);
|
|
});
|
|
});
|