/* Copyright 2022 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. * @module client */ import * as utils from "../test-utils"; import { DuplicateStrategy, EventStatus, MatrixEvent, PendingEventOrdering, RoomEvent } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; import { Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { RelationType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; import { Thread } from "../../src/models/thread"; 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; beforeEach(function() { room = new Room(roomId, null, userA); // mock RoomStates room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState"); room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState"); }); describe("getAvatarUrl", function() { const hsUrl = "https://my.home.server"; it("should return the URL from m.room.avatar preferentially", function() { room.currentState.getStateEvents.mockImplementation(function(type, key) { if (type === "m.room.avatar" && key === "") { return utils.mkEvent({ event: true, type: "m.room.avatar", skey: "", room: roomId, user: userA, content: { url: "mxc://flibble/wibble", }, }); } }); const url = room.getAvatarUrl(hsUrl); // 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); }); }); describe("getMember", function() { beforeEach(function() { room.currentState.getMember.mockImplementation(function(userId) { return { "@alice:bar": { userId: userA, roomId: roomId, }, }[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 = [ utils.mkMessage({ room: roomId, user: userA, msg: "changing room name", event: true, }), utils.mkEvent({ type: "m.room.name", room: roomId, user: userA, event: true, content: { name: "New Room Name" }, }), ]; it("should call RoomState.setTypingEvent on m.typing events", function() { const typing = utils.mkEvent({ room: roomId, type: "m.typing", event: true, content: { user_ids: [userA], }, }); room.addEphemeralEvents([typing]); expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing); }); it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() { expect(function() { room.addLiveEvents(events, "foo"); }).toThrow(); }); it("should replace a timeline event if dupe strategy is 'replace'", function() { // make a duplicate const dupe = utils.mkMessage({ room: roomId, user: userA, msg: "dupe", event: true, }); dupe.event.event_id = events[0].getId(); room.addLiveEvents(events); expect(room.timeline[0]).toEqual(events[0]); room.addLiveEvents([dupe], DuplicateStrategy.Replace); expect(room.timeline[0]).toEqual(dupe); }); it("should ignore a given dupe event if dupe strategy is 'ignore'", function() { // make a duplicate const dupe = utils.mkMessage({ room: roomId, user: userA, msg: "dupe", event: true, }); dupe.event.event_id = events[0].getId(); room.addLiveEvents(events); expect(room.timeline[0]).toEqual(events[0]); room.addLiveEvents([dupe], "ignore"); expect(room.timeline[0]).toEqual(events[0]); }); it("should emit 'Room.timeline' events", function() { let callCount = 0; room.on("Room.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(); }); room.addLiveEvents(events); expect(callCount).toEqual(2); }); it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events", function() { const events = [ utils.mkMembership({ room: roomId, mship: "invite", user: userB, skey: userA, event: true, }), utils.mkEvent({ type: "m.room.name", room: roomId, user: userB, event: true, content: { name: "New room", }, }), ]; room.addLiveEvents(events); expect(room.currentState.setStateEvents).toHaveBeenCalledWith( [events[0]], ); expect(room.currentState.setStateEvents).toHaveBeenCalledWith( [events[1]], ); 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", function() { const sentinel = { userId: userA, membership: "join", name: "Alice", }; room.currentState.getSentinelMember.mockImplementation(function(uid) { if (uid === userA) { return sentinel; } return null; }); room.addLiveEvents(events); expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId()); }); it("should emit Room.localEchoUpdated when a local echo is updated", 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(); let callCount = 0; room.on("Room.localEchoUpdated", function(event, emitRoom, oldEventId, oldStatus) { switch (callCount) { case 0: expect(event.getId()).toEqual(localEventId); expect(event.status).toEqual(EventStatus.SENDING); expect(emitRoom).toEqual(room); expect(oldEventId).toBe(null); expect(oldStatus).toBe(null); break; case 1: expect(event.getId()).toEqual(remoteEventId); expect(event.status).toBe(null); expect(emitRoom).toEqual(room); expect(oldEventId).toEqual(localEventId); expect(oldStatus).toBe(EventStatus.SENDING); break; } callCount += 1; }, ); // first add the local echo room.addPendingEvent(localEvent, "TXN_ID"); expect(room.timeline.length).toEqual(1); // then the remoteEvent room.addLiveEvents([remoteEvent]); expect(room.timeline.length).toEqual(1); expect(callCount).toEqual(2); }); }); describe("addEventsToTimeline", function() { const events = [ utils.mkMessage({ room: roomId, user: userA, msg: "changing room name", event: true, }), utils.mkEvent({ type: "m.room.name", 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, room.getLiveTimeline()); }).toThrow(); }); it("should be able to add events to the start", function() { room.addEventsToTimeline(events, true, 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("Room.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, room.getLiveTimeline()); expect(callCount).toEqual(2); }); }); describe("event metadata handling", function() { it("should set event.sender for new and old events", function() { const sentinel = { userId: userA, membership: "join", name: "Alice", }; const oldSentinel = { userId: userA, membership: "join", name: "Old Alice", }; room.currentState.getSentinelMember.mockImplementation(function(uid) { if (uid === userA) { return sentinel; } return null; }); room.oldState.getSentinelMember.mockImplementation(function(uid) { if (uid === userA) { return oldSentinel; } return null; }); const newEv = utils.mkEvent({ type: "m.room.name", room: roomId, user: userA, event: true, content: { name: "New Room Name" }, }); const oldEv = utils.mkEvent({ type: "m.room.name", room: roomId, user: userA, event: true, content: { name: "Old Room Name" }, }); room.addLiveEvents([newEv]); expect(newEv.sender).toEqual(sentinel); room.addEventsToTimeline([oldEv], true, room.getLiveTimeline()); expect(oldEv.sender).toEqual(oldSentinel); }); it("should set event.target for new and old m.room.member events", function() { const sentinel = { userId: userA, membership: "join", name: "Alice", }; const oldSentinel = { userId: userA, membership: "join", name: "Old Alice", }; room.currentState.getSentinelMember.mockImplementation(function(uid) { if (uid === userA) { return sentinel; } return null; }); room.oldState.getSentinelMember.mockImplementation(function(uid) { if (uid === userA) { return oldSentinel; } return null; }); const newEv = utils.mkMembership({ room: roomId, mship: "invite", user: userB, skey: userA, event: true, }); const oldEv = utils.mkMembership({ room: roomId, mship: "ban", user: userB, skey: userA, event: true, }); room.addLiveEvents([newEv]); expect(newEv.target).toEqual(sentinel); room.addEventsToTimeline([oldEv], true, 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 = [ utils.mkMembership({ room: roomId, mship: "invite", user: userB, skey: userA, event: true, }), utils.mkEvent({ type: "m.room.name", room: roomId, user: userB, event: true, content: { name: "New room", }, }), ]; room.addEventsToTimeline(events, true, room.getLiveTimeline()); expect(room.oldState.setStateEvents).toHaveBeenCalledWith( [events[0]], ); expect(room.oldState.setStateEvents).toHaveBeenCalledWith( [events[1]], ); expect(events[0].forwardLooking).toBe(false); expect(events[1].forwardLooking).toBe(false); expect(room.currentState.setStateEvents).not.toHaveBeenCalled(); }); }); const resetTimelineTests = function(timelineSupport) { let events = null; beforeEach(function() { room = new Room(roomId, null, null, { 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: "m.room.name", room: roomId, user: userA, event: true, content: { name: "New Room Name" }, }), utils.mkEvent({ type: "m.room.name", room: roomId, user: userA, event: true, content: { name: "Another New Name" }, }), ]; }); it("should copy state from previous timeline", function() { room.addLiveEvents([events[0], events[1]]); expect(room.getLiveTimeline().getEvents().length).toEqual(2); room.resetLiveTimeline('sometoken', 'someothertoken'); room.addLiveEvents([events[2]]); const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); expect(room.getLiveTimeline().getEvents().length).toEqual(1); expect(oldState.getStateEvents("m.room.name", "")).toEqual(events[1]); expect(newState.getStateEvents("m.room.name", "")).toEqual(events[2]); }); it("should reset the legacy timeline fields", function() { room.addLiveEvents([events[0], events[1]]); expect(room.timeline.length).toEqual(2); room.resetLiveTimeline('sometoken', 'someothertoken'); room.addLiveEvents([events[2]]); 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)); }); it("should emit Room.timelineReset event and set the correct " + "pagination token", function() { let callCount = 0; room.on("Room.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", function() { room.addLiveEvents([events[0]]); 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 timelinesupport enabled", resetTimelineTests.bind(null, true)); describe("resetLiveTimeline with timelinesupport disabled", resetTimelineTests.bind(null, false)); describe("compareEventOrdering", function() { beforeEach(function() { room = new Room(roomId, null, null, { timelineSupport: true }); }); const events = [ 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", function() { room.addLiveEvents(events); 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", function() { const oldTimeline = room.addTimeline(); oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), 'f'); room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b'); room.addEventsToTimeline([events[0]], false, oldTimeline); room.addLiveEvents([events[1]]); 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", function() { const oldTimeline = room.addTimeline(); room.addEventsToTimeline([events[0]], false, oldTimeline); room.addLiveEvents([events[1]]); 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", function() { room.addLiveEvents(events); 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() { room.currentState.getMembers.mockImplementation(function() { return [ { userId: "@alice:bar", membership: "join" }, { userId: "@bob:bar", membership: "invite" }, { userId: "@cleo:bar", membership: "leave" }, ]; }); 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() { room.currentState.getMembers.mockImplementation(function() { return [ { userId: "@bob:bar", membership: "invite" }, ]; }); const res = room.getJoinedMembers(); expect(res.length).toEqual(0); }); }); describe("hasMembershipState", function() { it("should return true for a matching userId and membership", function() { room.currentState.getMember.mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, "@bob:bar": { userId: "@bob:bar", membership: "invite" }, }[userId]; }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true); }); it("should return false if match membership but no match userId", function() { room.currentState.getMember.mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, }[userId]; }); expect(room.hasMembershipState("@bob:bar", "join")).toBe(false); }); it("should return false if match userId but no match membership", function() { room.currentState.getMember.mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, }[userId]; }); expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false); }); it("should return false if no match membership or userId", function() { room.currentState.getMember.mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, }[userId]; }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false); }); it("should return false if no members exist", function() { expect(room.hasMembershipState("@foo:bar", "join")).toBe(false); }); }); describe("recalculate", function() { const setJoinRule = function(rule) { room.addLiveEvents([utils.mkEvent({ type: "m.room.join_rules", room: roomId, user: userA, content: { join_rule: rule, }, event: true, })]); }; const setAltAliases = function(aliases) { room.addLiveEvents([utils.mkEvent({ type: "m.room.canonical_alias", room: roomId, skey: "", content: { alt_aliases: aliases, }, event: true, })]); }; const setAlias = function(alias) { room.addLiveEvents([utils.mkEvent({ type: "m.room.canonical_alias", room: roomId, skey: "", content: { alias }, event: true, })]); }; const setRoomName = function(name) { room.addLiveEvents([utils.mkEvent({ type: "m.room.name", room: roomId, user: userA, content: { name: name, }, event: true, })]); }; const addMember = function(userId, state = "join", opts: any = {}) { opts.room = roomId; opts.mship = state; opts.user = opts.user || userId; opts.skey = userId; opts.event = true; const event = utils.mkMembership(opts); room.addLiveEvents([event]); return event; }; beforeEach(function() { // no mocking room = new Room(roomId, null, 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", function() { const roomName = "flibble"; const event = addMember(userA, "invite"); event.event.unsigned = {}; event.event.unsigned.invite_room_state = [ { type: "m.room.name", state_key: "", content: { name: roomName, }, }, ]; room.recalculate(); expect(room.name).toEqual(roomName); }); it("should not clobber state events if it isn't an invite room", function() { const event = addMember(userA, "join"); const roomName = "flibble"; setRoomName(roomName); const roomNameToIgnore = "ignoreme"; event.event.unsigned = {}; event.event.unsigned.invite_room_state = [ { type: "m.room.name", state_key: "", content: { name: roomNameToIgnore, }, }, ]; 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, "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, "invite"); addMember(userB, "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, "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, "join", { name: nameB }); addMember(userC, "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, "join", { name: nameB }); addMember(userC, "join"); room.setSummary({ "m.heroes": [userB], }); room.recalculate(); expect(room.name).toEqual(nameB); }); 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"); }); }); 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("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("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("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("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("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("invite"); addMember(userA, "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("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("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("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("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("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("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("invite"); addMember(userB, 'join', { name: "Alice" }); addMember(userA, "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("invite"); addMember(userB); addMember(userA, "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, records) { const content = {}; 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, type, userId, ts) { ts = ts || Date.now(); return { eventId: eventId, type: type, userId: userId, ts: ts, }; } describe("addReceipt", function() { 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("Room.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", function() { const events = [ 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, }), ]; room.addLiveEvents(events); 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", () => { const events = [ 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, }), ]; room.addLiveEvents(events); 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("tags", function() { function mkTags(roomId, tags) { 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("Room.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'", 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, 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, }); room.addLiveEvents([eventA]); room.addPendingEvent(eventB, "TXN1"); room.addLiveEvents([eventC]); expect(room.timeline).toEqual( [eventA, eventC], ); expect(room.getPendingEvents()).toEqual( [eventB], ); }); it("should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'", function() { room = new Room(roomId, null, 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, }); room.addLiveEvents([eventA]); room.addPendingEvent(eventB, "TXN1"); room.addLiveEvents([eventC]); expect(room.timeline).toEqual( [eventA, eventB, eventC], ); }); }); 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, storageResponse = null) { return { getEventMapper: function() { // events should already be MatrixEvents return function(event) {return event;}; }, isCryptoEnabled() { return true; }, isRoomEncrypted: function() { return false; }, http: { serverResponse, authedRequest: function() { if (this.serverResponse instanceof Error) { return Promise.reject(this.serverResponse); } else { return Promise.resolve({ chunk: this.serverResponse }); } }, }, store: { storageResponse, storedMembers: null, getOutOfBandMembers: function() { if (this.storageResponse instanceof Error) { return Promise.reject(this.storageResponse); } else { return Promise.resolve(this.storageResponse); } }, setOutOfBandMembers: function(roomId, memberEvents) { this.storedMembers = memberEvents; return Promise.resolve(); }, getSyncToken: () => "sync_token", }, }; } const memberEvent = utils.mkMembership({ user: "@user_a:bar", mship: "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: "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 }); let hasThrown = false; try { await room.loadMembersIfNeeded(); } catch (err) { hasThrown = true; } expect(hasThrown).toEqual(true); client.http.serverResponse = [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("invite"); expect(room.getMyMembership()).toEqual("invite"); }); it("should emit a Room.myMembership event on a change", function() { const room = new Room(roomId, null, userA); const events = []; room.on(RoomEvent.MyMembership, (_room, membership, oldMembership) => { events.push({ membership, oldMembership }); }); room.updateMyMembership("invite"); expect(room.getMyMembership()).toEqual("invite"); expect(events[0]).toEqual({ membership: "invite", oldMembership: null }); events.splice(0); //clear room.updateMyMembership("invite"); expect(events.length).toEqual(0); room.updateMyMembership("join"); expect(room.getMyMembership()).toEqual("join"); expect(events[0]).toEqual({ membership: "join", oldMembership: "invite" }); }); }); describe("guessDMUserId", function() { it("should return first hero id", function() { const room = new Room(roomId, null, 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", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([utils.mkMembership({ user: userB, mship: "join", room: roomId, event: true, })]); expect(room.guessDMUserId()).toEqual(userB); }); it("should return self if only member present", function() { const room = new Room(roomId, null, userA); expect(room.guessDMUserId()).toEqual(userA); }); }); 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("invite"); expect(room.maySendMessage()).toEqual(false); room.updateMyMembership("leave"); expect(room.maySendMessage()).toEqual(false); room.updateMyMembership("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, null, userA); expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); }); it("should return a display name if one other member is in the room", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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", }), ]); expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); it("should return a display name if one other member is banned", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", room: roomId, event: true, name: "User A", }), utils.mkMembership({ user: userB, mship: "ban", room: roomId, event: true, name: "User B", }), ]); expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)"); }); it("should return a display name if one other member is invited", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", room: roomId, event: true, name: "User A", }), utils.mkMembership({ user: userB, mship: "invite", room: roomId, event: true, name: "User B", }), ]); expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); it("should return 'Empty room (was User B)' if User B left the room", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", room: roomId, event: true, name: "User A", }), utils.mkMembership({ user: userB, mship: "leave", room: roomId, event: true, name: "User B", }), ]); expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)"); }); it("should return 'User B and User C' if in a room with two other users", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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: userC, mship: "join", room: roomId, event: true, name: "User C", }), ]); expect(room.getDefaultRoomName(userA)).toEqual("User B and User C"); }); it("should return 'User B and 2 others' if in a room with three other users", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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: userC, mship: "join", room: roomId, event: true, name: "User C", }), utils.mkMembership({ user: userD, mship: "join", room: roomId, event: true, name: "User D", }), ]); 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", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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: [], }, }), ]); expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); it("should return a display name (default behaviour) if service members is a number (invalid)", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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: 1, }, }), ]); expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); it("should return a display name (default behaviour) if service members is a string (invalid)", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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.getDefaultRoomName(userA)).toEqual("User B"); }); it("should return 'Empty room' if the only other member is a functional member", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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.getDefaultRoomName(userA)).toEqual("Empty room"); }); it("should return 'User B' if User B is the only other member who isn't a functional member", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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: userC, mship: "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], }, }), ]); expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); it("should return 'Empty room' if all other members are functional members", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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: userC, mship: "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], }, }), ]); expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); }); it("should not break if an unjoined user is marked as a service user", function() { const room = new Room(roomId, null, userA); room.addLiveEvents([ 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, user: userA, content: { service_members: [userC], }, }), ]); 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); }); 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": RelationType.Thread, "event_id": "$000", }, }, unsigned: { "age": 1, }, }); room.createThread(undefined, [eventWithoutARootEvent]); const rootEvent = new MatrixEvent({ event_id: "$666", room_id: roomId, content: {}, unsigned: { "age": 1, "m.relations": { [RelationType.Thread]: { latest_event: null, count: 1, current_user_participated: false, }, }, }, }); expect(() => room.createThread(rootEvent, [])).not.toThrow(); }); it("should not add events before server supports is known", function() { Thread.hasServerSideSupport = undefined; const rootEvent = new MatrixEvent({ event_id: "$666", room_id: roomId, content: {}, unsigned: { "age": 1, "m.relations": { [RelationType.Thread]: { latest_event: null, count: 1, current_user_participated: false, }, }, }, }); let age = 1; function mkEvt(id): MatrixEvent { return new MatrixEvent({ event_id: id, room_id: roomId, content: { "m.relates_to": { "rel_type": RelationType.Thread, "event_id": "$666", }, }, unsigned: { "age": age++, }, }); } const thread = room.createThread(rootEvent, []); expect(thread.length).toBe(0); thread.addEvent(mkEvt("$1")); expect(thread.length).toBe(0); Thread.hasServerSideSupport = true; thread.addEvent(mkEvt("$2")); expect(thread.length).toBeGreaterThan(0); }); }); }); });