diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts index 0e12e8d93e..9a66a4907a 100644 --- a/playwright/e2e/chat-export/html-export.spec.ts +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -96,7 +96,10 @@ test.describe("HTML Export", () => { // Send a bunch of messages to populate the room for (let i = 1; i < 10; i++) { - await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); + const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); + if (i == 1) { + await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); + } } // Wait for all the messages to be displayed diff --git a/playwright/e2e/read-receipts/index.ts b/playwright/e2e/read-receipts/index.ts index 2db4bcbecf..747717908b 100644 --- a/playwright/e2e/read-receipts/index.ts +++ b/playwright/e2e/read-receipts/index.ts @@ -222,14 +222,7 @@ export class MessageBuilder { threadId: !ev.isThreadRoot ? ev.threadRootId : undefined, })); const roomId = await room.evaluate((room) => room.roomId); - - await bot.sendEvent(roomId, threadId ?? null, "m.reaction", { - "m.relates_to": { - rel_type: "m.annotation", - event_id: id, - key: reaction, - }, - }); + await bot.reactToMessage(roomId, threadId, id, reaction); } })(this); } diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts index 7d62f42ae0..2dfe7484f5 100644 --- a/playwright/pages/client.ts +++ b/playwright/pages/client.ts @@ -143,6 +143,29 @@ export class Client { ); } + /** + * Send a reaction to to a message + * @param roomId ID of the room to send the reaction into + * @param threadId ID of the thread to send into or null for main timeline + * @param eventId Event ID of the message you are reacting to + * @param reaction The reaction text to send + * @returns + */ + public async reactToMessage( + roomId: string, + threadId: string | null, + eventId: string, + reaction: string, + ): Promise { + return this.sendEvent(roomId, threadId ?? null, "m.reaction", { + "m.relates_to": { + rel_type: "m.annotation", + event_id: eventId, + key: reaction, + }, + }); + } + /** * Create a room with given options. * @param options the options to apply when creating the room diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png index 9d301e7919..6a490c2157 100644 Binary files a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts index ed85ba8d67..053878886b 100644 --- a/src/utils/exportUtils/Exporter.ts +++ b/src/utils/exportUtils/Exporter.ts @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Direction, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; -import { MediaEventContent } from "matrix-js-sdk/src/types"; +import { Direction, MatrixEvent, Relations, Room } from "matrix-js-sdk/src/matrix"; +import { EventType, MediaEventContent, RelationType } from "matrix-js-sdk/src/types"; import { saveAs } from "file-saver"; import { logger } from "matrix-js-sdk/src/logger"; import sanitizeFilename from "sanitize-filename"; @@ -284,5 +284,13 @@ export default abstract class Exporter { return mxEv.getType() === attachmentTypes[0] || attachmentTypes.includes(mxEv.getContent().msgtype!); } + protected getRelationsForEvent = ( + eventId: string, + relationType: RelationType | string, + eventType: EventType | string, + ): Relations | undefined => { + return this.room.getUnfilteredTimelineSet().relations.getChildEventsForEvent(eventId, relationType, eventType); + }; + public abstract export(): Promise; } diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 9a16a9e44b..08e488e5ff 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -288,9 +288,10 @@ export default class HTMLExporter extends Exporter { permalinkCreator={this.permalinkCreator} lastSuccessful={false} isSelectedEvent={false} - showReactions={false} + showReactions={true} layout={Layout.Group} showReadReceipts={false} + getRelationsForEvent={this.getRelationsForEvent} /> diff --git a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts index 7d57866fcd..0fc96e4db7 100644 --- a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts +++ b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts @@ -7,19 +7,24 @@ Please see LICENSE files in the repository root for full details. */ import { + EventTimeline, + EventTimelineSet, EventType, IRoomEvent, MatrixClient, MatrixEvent, MsgType, + Relations, + RelationType, Room, RoomMember, RoomState, } from "matrix-js-sdk/src/matrix"; import fetchMock from "fetch-mock-jest"; import escapeHtml from "escape-html"; +import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container"; -import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../../test-utils"; +import { filterConsole, mkReaction, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../../test-utils"; import { ExportType, IExportOptions } from "../../../../src/utils/exportUtils/exportUtils"; import SdkConfig from "../../../../src/SdkConfig"; import HTMLExporter from "../../../../src/utils/exportUtils/HtmlExport"; @@ -123,6 +128,35 @@ describe("HTMLExport", () => { fetchMock.get(media.srcHttp!, body); } + function mockReactionForMessage(message: IRoomEvent): MatrixEvent { + const firstMessage = new MatrixEvent(message); + const reaction = mkReaction(firstMessage); + + const relationsContainer = { + getRelations: jest.fn(), + getChildEventsForEvent: jest.fn(), + } as unknown as RelationsContainer; + const relations = new Relations(RelationType.Annotation, EventType.Reaction, client); + relations.addEvent(reaction); + relationsContainer.getChildEventsForEvent = jest + .fn() + .mockImplementation( + (eventId: string, relationType: RelationType | string, eventType: EventType | string) => { + if (eventId === firstMessage.getId()) { + return relations; + } + }, + ); + + const timelineSet = { + relations: relationsContainer, + getLiveTimeline: () => timeline, + } as unknown as EventTimelineSet; + const timeline = new EventTimeline(timelineSet); + room.getUnfilteredTimelineSet = jest.fn().mockReturnValue(timelineSet); + return reaction; + } + it("should throw when created with invalid config for LastNMessages", async () => { expect( () => @@ -167,6 +201,7 @@ describe("HTMLExport", () => { body: `Message #${i}`, }, })); + mockReactionForMessage(events[0]); mockMessages(...events); const exporter = new HTMLExporter( @@ -587,4 +622,24 @@ describe("HTMLExport", () => { expect(await file.text()).toContain("testing testing"); expect(client.createMessagesRequest).not.toHaveBeenCalled(); }); + + it("should include reactions", async () => { + const reaction = mockReactionForMessage(EVENT_MESSAGE); + mockMessages(EVENT_MESSAGE); + const exporter = new HTMLExporter( + room, + ExportType.LastNMessages, + { + attachmentsIncluded: false, + maxSize: 1_024 * 1_024, + numberOfMessages: 40, + }, + () => {}, + ); + + await exporter.export(); + + const file = getMessageFile(exporter); + expect(await file.text()).toContain(reaction.getContent()["m.relates_to"]?.key); + }); }); diff --git a/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index cd1f63aa8b..6a7adbabb2 100644 --- a/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -62,7 +62,7 @@ exports[`HTMLExport should export 1`] = `

-
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0
  • +
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0