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`] = `
-
+