From 5fbc5af884c770c05215f72cf465901d8fbc310a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 4 Oct 2024 15:58:22 +0200 Subject: [PATCH] Add support for rendering media captions (#43) * Add support for rendering media captions * Run prettier * Deduplicate body props * Add basic test * Fix import order in test * Fix test? --- .../views/messages/MessageEvent.tsx | 57 +++++++++++------ src/components/views/messages/TextualBody.tsx | 11 ++++ src/utils/FileUtils.ts | 4 +- .../views/messages/MessageEvent-test.tsx | 63 ++++++++++++++++++- 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 1a5d09e415..e4f3cb83ec 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -190,25 +190,42 @@ export default class MessageEvent extends React.Component implements IMe } } - return BodyType ? ( - - ) : null; + const hasCaption = + [MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video].includes(msgtype as MsgType) && + content.filename && + content.filename !== content.body; + const bodyProps: IBodyProps = { + ref: this.body, + mxEvent: this.props.mxEvent, + highlights: this.props.highlights, + highlightLink: this.props.highlightLink, + showUrlPreview: this.props.showUrlPreview, + forExport: this.props.forExport, + maxImageHeight: this.props.maxImageHeight, + replacingEventId: this.props.replacingEventId, + editState: this.props.editState, + onHeightChanged: this.props.onHeightChanged, + onMessageAllowed: this.onTileUpdate, + permalinkCreator: this.props.permalinkCreator, + mediaEventHelper: this.mediaHelper, + getRelationsForEvent: this.props.getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration: this.props.isSeeingThroughMessageHiddenForModeration, + inhibitInteraction: this.props.inhibitInteraction, + }; + if (hasCaption) { + return ; + } + + return BodyType ? : null; } } + +const CaptionBody: React.FunctionComponent }> = ({ + WrappedBodyType, + ...props +}) => ( +
+ + +
+); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 8b7bfb9a5a..890316fc02 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -556,6 +556,9 @@ export default class TextualBody extends React.Component { const content = mxEvent.getContent(); const isNotice = content.msgtype === MsgType.Notice; const isEmote = content.msgtype === MsgType.Emote; + const isCaption = [MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video].includes( + content.msgtype as MsgType, + ); const willHaveWrapper = this.props.replacingEventId || this.props.isSeeingThroughMessageHiddenForModeration || isEmote; @@ -635,6 +638,14 @@ export default class TextualBody extends React.Component { ); } + if (isCaption) { + return ( +
+ {body} + {widgets} +
+ ); + } return (
{body} diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 194cb31d20..37fd181900 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -38,7 +38,9 @@ export function presentableTextForFile( shortened = false, ): string { let text = fallbackText; - if (content.body?.length) { + if (content.filename?.length) { + text = content.filename; + } else if (content.body?.length) { // The content body should be the name of the file including a // file extension. text = content.body; diff --git a/test/components/views/messages/MessageEvent-test.tsx b/test/components/views/messages/MessageEvent-test.tsx index 16ed44f9a3..95481a0b12 100644 --- a/test/components/views/messages/MessageEvent-test.tsx +++ b/test/components/views/messages/MessageEvent-test.tsx @@ -8,7 +8,10 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { render, RenderResult } from "@testing-library/react"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, EventType, Room, MsgType } from "matrix-js-sdk/src/matrix"; +import fetchMock from "fetch-mock-jest"; +import fs from "fs"; +import path from "path"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; @@ -25,6 +28,26 @@ jest.mock("../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => VoiceBroadcastBody: () =>
, })); +jest.mock("../../../../src/components/views/messages/MImageBody", () => ({ + __esModule: true, + default: () =>
, +})); + +jest.mock("../../../../src/components/views/messages/MImageReplyBody", () => ({ + __esModule: true, + default: () =>
, +})); + +jest.mock("../../../../src/components/views/messages/MStickerBody", () => ({ + __esModule: true, + default: () =>
, +})); + +jest.mock("../../../../src/components/views/messages/TextualBody.tsx", () => ({ + __esModule: true, + default: () =>
, +})); + describe("MessageEvent", () => { let room: Room; let client: MatrixClient; @@ -68,4 +91,42 @@ describe("MessageEvent", () => { result.getByTestId("voice-broadcast-body"); }); }); + + describe("when an image with a caption is sent", () => { + let result: RenderResult; + + beforeEach(() => { + event = mkEvent({ + event: true, + type: EventType.RoomMessage, + user: client.getUserId()!, + room: room.roomId, + content: { + body: "caption for a test image", + format: "org.matrix.custom.html", + formatted_body: "caption for a test image", + msgtype: MsgType.Image, + filename: "image.webp", + info: { + w: 40, + h: 50, + }, + url: "mxc://server/image", + }, + }); + result = renderMessageEvent(); + }); + + it("should render a TextualBody and an ImageBody", () => { + fetchMock.getOnce( + "https://server/_matrix/media/v3/download/server/image", + { + body: fs.readFileSync(path.resolve(__dirname, "..", "..", "..", "images", "animated-logo.webp")), + }, + { sendAsJson: false }, + ); + result.getByTestId("image-body"); + result.getByTestId("textual-body"); + }); + }); });