diff --git a/spec/integ/matrix-client-event-timeline.spec.js b/spec/integ/matrix-client-event-timeline.spec.ts similarity index 86% rename from spec/integ/matrix-client-event-timeline.spec.js rename to spec/integ/matrix-client-event-timeline.spec.ts index c165a7057ed..3bde9dd6da3 100644 --- a/spec/integ/matrix-client-event-timeline.spec.js +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1,5 +1,21 @@ +/* +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. +*/ + import * as utils from "../test-utils/test-utils"; -import { EventTimeline, Filter, MatrixEvent } from "../../src/matrix"; +import { ClientEvent, EventTimeline, Filter, IEvent, MatrixClient, MatrixEvent, Room } from "../../src/matrix"; import { logger } from "../../src/logger"; import { TestClient } from "../TestClient"; import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; @@ -10,8 +26,14 @@ const accessToken = "aseukfgwef"; const roomId = "!foo:bar"; const otherUserId = "@bob:localhost"; +const withoutRoomId = (e: Partial): Partial => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { room_id: _, ...copy } = e; + return copy; +}; + const USER_MEMBERSHIP_EVENT = utils.mkMembership({ - room: roomId, mship: "join", user: userId, name: userName, + room: roomId, mship: "join", user: userId, name: userName, event: false, }); const ROOM_NAME_EVENT = utils.mkEvent({ @@ -19,34 +41,37 @@ const ROOM_NAME_EVENT = utils.mkEvent({ content: { name: "Old room name", }, + event: false, }); const INITIAL_SYNC_DATA = { next_batch: "s_5_3", rooms: { join: { - "!foo:bar": { // roomId + [roomId]: { timeline: { events: [ utils.mkMessage({ - room: roomId, user: otherUserId, msg: "hello", + user: otherUserId, msg: "hello", event: false, }), ], prev_batch: "f_1_1", }, state: { events: [ - ROOM_NAME_EVENT, + withoutRoomId(ROOM_NAME_EVENT), utils.mkMembership({ - room: roomId, mship: "join", + mship: "join", user: otherUserId, name: "Bob", + event: false, }), - USER_MEMBERSHIP_EVENT, + withoutRoomId(USER_MEMBERSHIP_EVENT), utils.mkEvent({ - type: "m.room.create", room: roomId, user: userId, + type: "m.room.create", user: userId, content: { creator: userId, }, + event: false, }), ], }, @@ -57,16 +82,16 @@ const INITIAL_SYNC_DATA = { const EVENTS = [ utils.mkMessage({ - room: roomId, user: userId, msg: "we", + room: roomId, user: userId, msg: "we", event: false, }), utils.mkMessage({ - room: roomId, user: userId, msg: "could", + room: roomId, user: userId, msg: "could", event: false, }), utils.mkMessage({ - room: roomId, user: userId, msg: "be", + room: roomId, user: userId, msg: "be", event: false, }), utils.mkMessage({ - room: roomId, user: userId, msg: "heroes", + room: roomId, user: userId, msg: "heroes", event: false, }), ]; @@ -81,12 +106,13 @@ const THREAD_ROOT = utils.mkEvent({ unsigned: { "m.relations": { "io.element.thread": { - "latest_event": undefined, + //"latest_event": undefined, "count": 1, "current_user_participated": true, }, }, }, + event: false, }); const THREAD_REPLY = utils.mkEvent({ @@ -102,12 +128,25 @@ const THREAD_REPLY = utils.mkEvent({ event_id: THREAD_ROOT.event_id, }, }, + event: false, }); THREAD_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD_REPLY; +const SYNC_THREAD_ROOT = withoutRoomId(THREAD_ROOT); +const SYNC_THREAD_REPLY = withoutRoomId(THREAD_REPLY); +SYNC_THREAD_ROOT.unsigned = { + "m.relations": { + "io.element.thread": { + "latest_event": SYNC_THREAD_REPLY, + "count": 1, + "current_user_participated": true, + }, + }, +}; + // start the client, and wait for it to initialise -function startClient(httpBackend, client) { +function startClient(httpBackend: TestClient["httpBackend"], client: MatrixClient) { httpBackend.when("GET", "/versions").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); @@ -116,8 +155,8 @@ function startClient(httpBackend, client) { client.startClient(); // set up a promise which will resolve once the client is initialised - const prom = new Promise((resolve) => { - client.on("sync", function(state) { + const prom = new Promise((resolve) => { + client.on(ClientEvent.Sync, function(state) { logger.log("sync", state); if (state != "SYNCING") { return; @@ -133,8 +172,8 @@ function startClient(httpBackend, client) { } describe("getEventTimeline support", function() { - let httpBackend; - let client; + let httpBackend: TestClient["httpBackend"]; + let client: MatrixClient; beforeEach(function() { const testClient = new TestClient(userId, "DEVICE", accessToken); @@ -177,7 +216,7 @@ describe("getEventTimeline support", function() { it("scrollback should be able to scroll back to before a gappy /sync", function() { // need a client with timelineSupport disabled to make this work - let room; + let room: Room; return startClient(httpBackend, client).then(function() { room = client.getRoom(roomId); @@ -189,7 +228,7 @@ describe("getEventTimeline support", function() { "!foo:bar": { timeline: { events: [ - EVENTS[0], + withoutRoomId(EVENTS[0]), ], prev_batch: "f_1_1", }, @@ -205,7 +244,7 @@ describe("getEventTimeline support", function() { "!foo:bar": { timeline: { events: [ - EVENTS[1], + withoutRoomId(EVENTS[1]), ], limited: true, prev_batch: "f_1_2", @@ -240,8 +279,8 @@ describe("getEventTimeline support", function() { }); describe("MatrixClient event timelines", function() { - let client = null; - let httpBackend = null; + let client: MatrixClient; + let httpBackend: TestClient["httpBackend"]; beforeEach(function() { const testClient = new TestClient( @@ -260,7 +299,7 @@ describe("MatrixClient event timelines", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); - Thread.setServerSideSupport(false); + Thread.setServerSideSupport(false, false); }); describe("getEventTimeline", function() { @@ -308,7 +347,7 @@ describe("MatrixClient event timelines", function() { "!foo:bar": { timeline: { events: [ - EVENTS[0], + withoutRoomId(EVENTS[0]), ], prev_batch: "f_1_2", }, @@ -343,7 +382,7 @@ describe("MatrixClient event timelines", function() { "!foo:bar": { timeline: { events: [ - EVENTS[3], + withoutRoomId(EVENTS[3]), ], prev_batch: "f_1_2", }, @@ -366,7 +405,7 @@ describe("MatrixClient event timelines", function() { }); const prom = new Promise((resolve, reject) => { - client.on("sync", function() { + client.on(ClientEvent.Sync, function() { client.getEventTimeline(timelineSet, EVENTS[2].event_id, ).then(function(tl) { expect(tl.getEvents().length).toEqual(4); @@ -511,8 +550,9 @@ describe("MatrixClient event timelines", function() { }); it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => { + // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true); + Thread.setServerSideSupport(true, false); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false); @@ -556,8 +596,9 @@ describe("MatrixClient event timelines", function() { }); it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => { + // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true); + Thread.setServerSideSupport(true, false); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const threadRoot = new MatrixEvent(THREAD_ROOT); @@ -587,8 +628,9 @@ describe("MatrixClient event timelines", function() { }); it("should return undefined when event is not in the thread that the given timelineSet is representing", () => { + // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true); + Thread.setServerSideSupport(true, false); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const threadRoot = new MatrixEvent(THREAD_ROOT); @@ -614,8 +656,9 @@ describe("MatrixClient event timelines", function() { }); it("should return undefined when event is within a thread but timelineSet is not", () => { + // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true); + Thread.setServerSideSupport(true, false); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; @@ -639,6 +682,7 @@ describe("MatrixClient event timelines", function() { }); it("should should add lazy loading filter when requested", async () => { + // @ts-ignore client.clientOpts.lazyLoadMembers = true; client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); @@ -656,7 +700,7 @@ describe("MatrixClient event timelines", function() { }; }); req.check((request) => { - expect(request.opts.qs.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)); + expect(request.queryParams.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)); }); await Promise.all([ @@ -863,7 +907,7 @@ describe("MatrixClient event timelines", function() { "!foo:bar": { timeline: { events: [ - event, + withoutRoomId(event), ], prev_batch: "f_1_1", }, @@ -941,11 +985,10 @@ describe("MatrixClient event timelines", function() { // a state event, followed by a redaction thereof const event = utils.mkMembership({ - room: roomId, mship: "join", user: otherUserId, + mship: "join", user: otherUserId, }); const redaction = utils.mkEvent({ type: "m.room.redaction", - room_id: roomId, sender: otherUserId, content: {}, }); @@ -987,7 +1030,7 @@ describe("MatrixClient event timelines", function() { timeline: { events: [ utils.mkMessage({ - room: roomId, user: otherUserId, msg: "world", + user: otherUserId, msg: "world", }), ], limited: true, @@ -1006,4 +1049,75 @@ describe("MatrixClient event timelines", function() { expect(tl.getEvents().length).toEqual(1); }); }); + + it("should re-insert room IDs for bundled thread relation events", async () => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(true, false); + + httpBackend.when("GET", "/sync").respond(200, { + next_batch: "s_5_4", + rooms: { + join: { + [roomId]: { + timeline: { + events: [ + SYNC_THREAD_ROOT, + ], + prev_batch: "f_1_1", + }, + }, + }, + }, + }); + await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]); + + const room = client.getRoom(roomId); + const thread = room.getThread(THREAD_ROOT.event_id); + const timelineSet = thread.timelineSet; + + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id)) + .respond(200, { + start: "start_token", + events_before: [], + event: THREAD_ROOT, + events_after: [], + state: [], + end: "end_token", + }); + httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + encodeURIComponent(THREAD_ROOT.event_id) + "/" + + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20") + .respond(200, function() { + return { + original_event: THREAD_ROOT, + chunk: [THREAD_REPLY], + // no next batch as this is the oldest end of the timeline + }; + }); + await Promise.all([ + client.getEventTimeline(timelineSet, THREAD_ROOT.event_id), + httpBackend.flushAllExpected(), + ]); + + httpBackend.when("GET", "/sync").respond(200, { + next_batch: "s_5_5", + rooms: { + join: { + [roomId]: { + timeline: { + events: [ + SYNC_THREAD_REPLY, + ], + prev_batch: "f_1_2", + }, + }, + }, + }, + }); + + await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]); + + expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY); + }); }); diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index 84a9662e419..4e0a311a0aa 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -70,7 +70,7 @@ export function mock(constr: { new(...args: any[]): T }, name: string): T { interface IEventOpts { type: EventType | string; - room: string; + room?: string; sender?: string; skey?: string; content: IContent; @@ -93,8 +93,8 @@ let testEventIndex = 1; // counter for events, easier for comparison of randomly * @return {Object} a JSON object representing this event. */ export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent; -export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): object; -export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): object | MatrixEvent { +export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial; +export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): Partial | MatrixEvent { if (!opts.type || !opts.content) { throw new Error("Missing .type or .content =>" + JSON.stringify(opts)); } @@ -145,8 +145,8 @@ interface IPresenceOpts { * @return {Object|MatrixEvent} The event */ export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent; -export function mkPresence(opts: IPresenceOpts & { event?: false }): object; -export function mkPresence(opts: IPresenceOpts & { event?: boolean }): object | MatrixEvent { +export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial; +export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial | MatrixEvent { const event = { event_id: "$" + Math.random() + "-" + Math.random(), type: "m.presence", @@ -162,7 +162,7 @@ export function mkPresence(opts: IPresenceOpts & { event?: boolean }): object | } interface IMembershipOpts { - room: string; + room?: string; mship: string; sender?: string; user?: string; @@ -186,8 +186,8 @@ interface IMembershipOpts { * @return {Object|MatrixEvent} The event */ export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent; -export function mkMembership(opts: IMembershipOpts & { event?: false }): object; -export function mkMembership(opts: IMembershipOpts & { event?: boolean }): object | MatrixEvent { +export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial; +export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Partial | MatrixEvent { const eventOpts: IEventOpts = { ...opts, type: EventType.RoomMember, @@ -209,7 +209,7 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): objec } interface IMessageOpts { - room: string; + room?: string; user: string; msg?: string; event?: boolean; @@ -226,8 +226,11 @@ interface IMessageOpts { * @return {Object|MatrixEvent} The event */ export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent; -export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): object; -export function mkMessage(opts: IMessageOpts & { event?: boolean }, client?: MatrixClient): object | MatrixEvent { +export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial; +export function mkMessage( + opts: IMessageOpts & { event?: boolean }, + client?: MatrixClient, +): Partial | MatrixEvent { const eventOpts: IEventOpts = { ...opts, type: EventType.RoomMessage, @@ -260,11 +263,11 @@ interface IReplyMessageOpts extends IMessageOpts { * @return {Object|MatrixEvent} The event */ export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent; -export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): object; +export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial; export function mkReplyMessage( opts: IReplyMessageOpts & { event?: boolean }, client?: MatrixClient, -): object | MatrixEvent { +): Partial | MatrixEvent { const eventOpts: IEventOpts = { ...opts, type: EventType.RoomMessage, diff --git a/src/models/thread.ts b/src/models/thread.ts index aa6c8ae38e7..c451ccb8dc2 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -279,7 +279,10 @@ export class Thread extends TypedEventEmitter { this.replyCount = bundledRelationship.count; this._currentUserParticipated = bundledRelationship.current_user_participated; - const event = new MatrixEvent(bundledRelationship.latest_event); + const event = new MatrixEvent({ + room_id: this.rootEvent.getRoomId(), + ...bundledRelationship.latest_event, + }); this.setEventMetadata(event); event.setThread(this); this.lastEvent = event;