diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index 4486e69a5d..914555497e 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -238,7 +238,6 @@ @import "./views/messages/_RoomAvatarEvent.pcss"; @import "./views/messages/_TextualEvent.pcss"; @import "./views/messages/_ThreadActionBar.pcss"; -@import "./views/messages/_UnknownBody.pcss"; @import "./views/messages/_ViewSourceEvent.pcss"; @import "./views/messages/_common_CryptoEvent.pcss"; @import "./views/polls/pollHistory/_PollHistory.pcss"; diff --git a/apps/web/res/css/views/messages/_UnknownBody.pcss b/apps/web/res/css/views/messages/_UnknownBody.pcss deleted file mode 100644 index 9583f55734..0000000000 --- a/apps/web/res/css/views/messages/_UnknownBody.pcss +++ /dev/null @@ -1,11 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_UnknownBody { - white-space: pre-wrap; -} diff --git a/apps/web/src/components/views/messages/MessageEvent.tsx b/apps/web/src/components/views/messages/MessageEvent.tsx index d8de22a427..58d7884a3f 100644 --- a/apps/web/src/components/views/messages/MessageEvent.tsx +++ b/apps/web/src/components/views/messages/MessageEvent.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import mime from "mime"; -import React, { createRef } from "react"; +import React, { createRef, type JSX } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EventType, @@ -18,10 +18,10 @@ import { M_POLL_START, type IContent, } from "matrix-js-sdk/src/matrix"; +import { UnknownBodyView } from "@element-hq/web-shared-components"; import SettingsStore from "../../../settings/SettingsStore"; import { Mjolnir } from "../../../mjolnir/Mjolnir"; -import UnknownBody from "./UnknownBody"; import { type IMediaBody } from "./IMediaBody"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { type IBodyProps } from "./IBodyProps"; @@ -80,6 +80,10 @@ const baseEvTypes = new Map>([ [M_BEACON_INFO.altName, MBeaconBody], ]); +function UnknownBody({ mxEvent, ref }: IBodyProps): JSX.Element { + return ; +} + export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { private body = createRef(); private mediaHelper?: MediaEventHelper; diff --git a/apps/web/src/components/views/messages/UnknownBody.tsx b/apps/web/src/components/views/messages/UnknownBody.tsx deleted file mode 100644 index 26e4ea5fca..0000000000 --- a/apps/web/src/components/views/messages/UnknownBody.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2020 The Matrix.org Foundation C.I.C. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type JSX } from "react"; - -import { type IBodyProps } from "./IBodyProps"; - -export default ({ mxEvent, ref }: IBodyProps): JSX.Element => { - const text = mxEvent.getContent().body; - return ( -
- {text} -
- ); -}; diff --git a/apps/web/test/unit-tests/components/views/messages/MessageEvent-test.tsx b/apps/web/test/unit-tests/components/views/messages/MessageEvent-test.tsx index 9bc87cf7b7..667a3c50b3 100644 --- a/apps/web/test/unit-tests/components/views/messages/MessageEvent-test.tsx +++ b/apps/web/test/unit-tests/components/views/messages/MessageEvent-test.tsx @@ -19,11 +19,6 @@ import MessageEvent from "../../../../../src/components/views/messages/MessageEv import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -jest.mock("../../../../../src/components/views/messages/UnknownBody", () => ({ - __esModule: true, - default: () =>
, -})); - jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({ __esModule: true, default: () =>
, @@ -118,6 +113,25 @@ describe("MessageEvent", () => { expect(result.queryByTestId("textual-body")).toBeNull(); }); + it("renders the shared unknown body for unsupported message types", () => { + event = mkEvent({ + event: true, + type: EventType.RoomMessage, + user: "@alice:example.com", + room: room.roomId, + content: { + msgtype: "org.example.unsupported", + body: "Unsupported message body", + }, + }); + + const result = renderMessageEvent(); + + expect(result.getByText("Unsupported message body")).toBeInTheDocument(); + expect(result.container.querySelector(".mx_UnknownBody")).not.toBeNull(); + expect(result.queryByTestId("textual-body")).toBeNull(); + }); + describe("when an image with a caption is sent", () => { let result: RenderResult; diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx/default-auto.png new file mode 100644 index 0000000000..3faed1727f Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx/multiline-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx/multiline-auto.png new file mode 100644 index 0000000000..0889587c07 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx/multiline-auto.png differ diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 04ac211dcb..234382b222 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -24,6 +24,7 @@ export * from "./room/timeline/event-tile/body/MFileBodyView"; export * from "./room/timeline/event-tile/body/MImageBodyView"; export * from "./room/timeline/event-tile/body/MVideoBodyView"; export * from "./room/timeline/event-tile/body/TextualBodyView"; +export * from "./room/timeline/event-tile/body/UnknownBodyView"; export * from "./room/timeline/event-tile/EventTileView/TileErrorView"; export * from "./core/pill-input/Pill"; export * from "./core/pill-input/PillInput"; diff --git a/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.module.css b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.module.css new file mode 100644 index 0000000000..a54ec03ad5 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.module.css @@ -0,0 +1,10 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.content { + white-space: pre-wrap; +} diff --git a/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx new file mode 100644 index 0000000000..4aaa867bd6 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.stories.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { withViewDocs } from "../../../../../../.storybook/withViewDocs"; +import { UnknownBodyView } from "./UnknownBodyView"; + +const UnknownBodyViewWrapper = withViewDocs(UnknownBodyView, UnknownBodyView); + +const meta = { + title: "Timeline/Timeline Body/UnknownBodyView", + component: UnknownBodyViewWrapper, + tags: ["autodocs"], + args: { + text: "Unsupported message body", + className: "", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Multiline: Story = { + args: { + text: "Unsupported message body\nwith preserved line breaks", + }, +}; diff --git a/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.test.tsx b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.test.tsx new file mode 100644 index 0000000000..34c5ed66b2 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { composeStories } from "@storybook/react-vite"; +import { render, screen } from "@test-utils"; +import React from "react"; +import { describe, expect, it } from "vitest"; + +import { UnknownBodyView } from "./UnknownBodyView"; +import * as stories from "./UnknownBodyView.stories"; + +const { Default, Multiline } = composeStories(stories); + +describe("UnknownBodyView", () => { + it("renders the default story", () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + expect(screen.getByText("Unsupported message body")).toBeInTheDocument(); + }); + + it("renders multiline content", () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + expect(screen.getByText(/with preserved line breaks/)).toBeInTheDocument(); + }); + + it("applies a custom className to the root element", () => { + const { container } = render(); + + expect(container.firstChild).toHaveClass("custom-unknown"); + }); + + it("forwards the provided ref to the root element", () => { + const ref = React.createRef(); + + render(); + + expect(ref.current).toBeInstanceOf(HTMLDivElement); + expect(ref.current).toHaveTextContent("Unsupported message body"); + }); +}); diff --git a/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.tsx b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.tsx new file mode 100644 index 0000000000..ccd6b47f94 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/UnknownBodyView.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import classNames from "classnames"; +import React, { type JSX, type ReactNode, type Ref } from "react"; + +import styles from "./UnknownBodyView.module.css"; + +export interface UnknownBodyViewProps { + /** + * Fallback message body content. + */ + text?: ReactNode; + /** + * Optional CSS class names applied to the root element. + */ + className?: string; + /** + * Optional ref forwarded to the root element. + */ + ref?: Ref; +} + +/** + * Renders fallback body content for unsupported message types. + */ +export function UnknownBodyView({ text, className, ref }: Readonly): JSX.Element { + return ( +
+ {text} +
+ ); +} diff --git a/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/__snapshots__/UnknownBodyView.test.tsx.snap b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/__snapshots__/UnknownBodyView.test.tsx.snap new file mode 100644 index 0000000000..0c66280621 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/__snapshots__/UnknownBodyView.test.tsx.snap @@ -0,0 +1,22 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`UnknownBodyView > renders multiline content 1`] = ` +
+
+ Unsupported message body +with preserved line breaks +
+
+`; + +exports[`UnknownBodyView > renders the default story 1`] = ` +
+
+ Unsupported message body +
+
+`; diff --git a/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/index.tsx b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/index.tsx new file mode 100644 index 0000000000..49eded1c67 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/UnknownBodyView/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { UnknownBodyView, type UnknownBodyViewProps } from "./UnknownBodyView";