diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index d164272d91b..ff41b35960d 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -6,7 +6,7 @@ import "../olm-loader"; import { logger } from "../../src/logger"; import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event"; -import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src"; +import { ClientEvent, EventType, IPusher, MatrixClient, MsgType, RelationType } from "../../src"; import { SyncState } from "../../src/sync"; import { eventMapperFor } from "../../src/event-mapper"; @@ -258,6 +258,9 @@ export interface IMessageOpts { * @param opts.user - The user ID for the event. * @param opts.msg - Optional. The content.body for the event. * @param opts.event - True to make a MatrixEvent. + * @param opts.relatesTo - An IEventRelation relating this to another event. + * @param opts.ts - The timestamp of the event. + * @param opts.event - True to make a MatrixEvent. * @param client - If passed along with opts.event=true will be used to set up re-emitters. * @returns The event */ @@ -297,6 +300,7 @@ interface IReplyMessageOpts extends IMessageOpts { * @param opts.room - The room ID for the event. * @param opts.user - The user ID for the event. * @param opts.msg - Optional. The content.body for the event. + * @param opts.ts - The timestamp of the event. * @param opts.replyToMessage - The replied message * @param opts.event - True to make a MatrixEvent. * @param client - If passed along with opts.event=true will be used to set up re-emitters. @@ -330,6 +334,42 @@ export function mkReplyMessage( return mkEvent(eventOpts, client); } +/** + * Create a reaction event. + * + * @param target - the event we are reacting to. + * @param client - the MatrixClient + * @param userId - the userId of the sender + * @param roomId - the id of the room we are in + * @param ts - The timestamp of the event. + * @returns The event + */ +export function mkReaction( + target: MatrixEvent, + client: MatrixClient, + userId: string, + roomId: string, + ts?: number, +): MatrixEvent { + return mkEvent( + { + event: true, + type: EventType.Reaction, + user: userId, + room: roomId, + content: { + "m.relates_to": { + rel_type: RelationType.Annotation, + event_id: target.getId()!, + key: Math.random().toString(), + }, + }, + ts, + }, + client, + ); +} + /** * A mock implementation of webstorage */ diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index d1e25c84274..bd17f00c8aa 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -14,17 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { mocked } from "jest-mock"; + import { MatrixClient, PendingEventOrdering } from "../../../src/client"; import { Room } from "../../../src/models/room"; -import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread"; +import { Thread, THREAD_RELATION_TYPE, ThreadEvent, FeatureSupport } from "../../../src/models/thread"; import { mkThread } from "../../test-utils/thread"; import { TestClient } from "../../TestClient"; -import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils"; +import { emitPromise, mkMessage, mkReaction, mock } from "../../test-utils/test-utils"; import { Direction, EventStatus, MatrixEvent } from "../../../src"; import { ReceiptType } from "../../../src/@types/read_receipts"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client"; import { ReEmitter } from "../../../src/ReEmitter"; import { Feature, ServerSupport } from "../../../src/feature"; +import { eventMapperFor } from "../../../src/event-mapper"; describe("Thread", () => { describe("constructor", () => { @@ -424,4 +427,51 @@ describe("Thread", () => { expect(mock).toHaveBeenCalledWith("b1", "f1"); }); }); + + describe("insertEventIntoTimeline", () => { + it("Inserts a reply in timestamp order", () => { + // Assumption: no server side support because if we have it, events + // can only be added to the timeline after the thread has been + // initialised, and we are not properly initialising it here. + expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None); + + const client = createClientWithEventMapper(); + const userId = "user1"; + const room = new Room("room1", client, userId); + + // Given a thread with a root plus 5 messages + const { thread, events } = mkThread({ + room, + client, + authorId: userId, + participantUserIds: ["@bob:hs", "@chia:hs", "@dv:hs"], + length: 6, + ts: 100, // Events will be at ts 100, 101, 102, 103, 104 and 105 + }); + + // When we insert a reply to the second thread message + const replyEvent = mkReaction(events[2], client, userId, room.roomId, 104); + thread.insertEventIntoTimeline(replyEvent); + + // Then the reply is inserted based on its timestamp + expect(thread.events.map((ev) => ev.getId())).toEqual([ + events[0].getId(), + events[1].getId(), + events[2].getId(), + events[3].getId(), + events[4].getId(), + replyEvent.getId(), + events[5].getId(), + ]); + }); + + function createClientWithEventMapper(): MatrixClient { + const client = mock(MatrixClient, "MatrixClient"); + client.reEmitter = mock(ReEmitter, "ReEmitter"); + client.canSupport = new Map(); + jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {})); + mocked(client.supportsThreads).mockReturnValue(true); + return client; + } + }); }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 9f5479031cd..e168a5bf628 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -137,24 +137,6 @@ describe("Room", function () { room.client, ); - const mkReaction = (target: MatrixEvent) => - utils.mkEvent( - { - event: true, - type: EventType.Reaction, - user: userA, - room: roomId, - content: { - "m.relates_to": { - rel_type: RelationType.Annotation, - event_id: target.getId()!, - key: Math.random().toString(), - }, - }, - }, - room.client, - ); - const mkRedaction = (target: MatrixEvent) => utils.mkEvent( { @@ -2674,7 +2656,7 @@ describe("Room", function () { threadResponse1.localTimestamp += 1000; const threadResponse2 = mkThreadResponse(threadRoot); threadResponse2.localTimestamp += 2000; - const threadResponse2Reaction = mkReaction(threadResponse2); + const threadResponse2Reaction = utils.mkReaction(threadResponse2, room.client, userA, roomId); room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({ @@ -2714,7 +2696,7 @@ describe("Room", function () { threadResponse1.localTimestamp += 1000; const threadResponse2 = mkThreadResponse(threadRoot); threadResponse2.localTimestamp += 2000; - const threadResponse2Reaction = mkReaction(threadResponse2); + const threadResponse2Reaction = utils.mkReaction(threadResponse2, room.client, userA, roomId); room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({ @@ -2862,8 +2844,8 @@ describe("Room", function () { const randomMessage = mkMessage(); const threadRoot = mkMessage(); const threadResponse1 = mkThreadResponse(threadRoot); - const threadReaction1 = mkReaction(threadRoot); - const threadReaction2 = mkReaction(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()!]); @@ -2900,8 +2882,8 @@ describe("Room", function () { it("thread response and its relations&redactions should be only in thread timeline", () => { const threadRoot = mkMessage(); const threadResponse1 = mkThreadResponse(threadRoot); - const threadReaction1 = mkReaction(threadResponse1); - const threadReaction2 = mkReaction(threadResponse1); + 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()!]); @@ -2922,8 +2904,8 @@ describe("Room", function () { const threadRoot = mkMessage(); const threadResponse1 = mkThreadResponse(threadRoot); const reply1 = mkReply(threadResponse1); - const reaction1 = mkReaction(reply1); - const reaction2 = mkReaction(reply1); + const reaction1 = utils.mkReaction(reply1, room.client, userA, roomId); + const reaction2 = utils.mkReaction(reply1, room.client, userA, roomId); const reaction2Redaction = mkRedaction(reply1); const roots = new Set([threadRoot.getId()!]); @@ -2957,9 +2939,9 @@ describe("Room", function () { it("should aggregate relations in thread event timeline set", async () => { Thread.setServerSideSupport(FeatureSupport.Stable); const threadRoot = mkMessage(); - const rootReaction = mkReaction(threadRoot); + const rootReaction = utils.mkReaction(threadRoot, room.client, userA, roomId); const threadResponse = mkThreadResponse(threadRoot); - const threadReaction = mkReaction(threadResponse); + const threadReaction = utils.mkReaction(threadResponse, room.client, userA, roomId); const events = [threadRoot, rootReaction, threadResponse, threadReaction];