diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index 1813d36d34..4486e69a5d 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -223,7 +223,6 @@ @import "./views/messages/_CreateEvent.pcss"; @import "./views/messages/_DisambiguatedProfile.pcss"; @import "./views/messages/_HiddenBody.pcss"; -@import "./views/messages/_HiddenMediaPlaceholder.pcss"; @import "./views/messages/_LegacyCallEvent.pcss"; @import "./views/messages/_MFileBody.pcss"; @import "./views/messages/_MImageBody.pcss"; diff --git a/apps/web/res/css/views/messages/_HiddenMediaPlaceholder.pcss b/apps/web/res/css/views/messages/_HiddenMediaPlaceholder.pcss deleted file mode 100644 index c7efe6ec7e..0000000000 --- a/apps/web/res/css/views/messages/_HiddenMediaPlaceholder.pcss +++ /dev/null @@ -1,29 +0,0 @@ -.mx_HiddenMediaPlaceholder { - border: none; - width: 100%; - height: 100%; - inset: 0; - - /* To center the text in the middle of the frame */ - display: flex; - align-items: center; - justify-content: center; - text-align: center; - - cursor: pointer; - background-color: $header-panel-bg-color; - - > div { - color: $accent; - /* Icon alignment */ - display: flex; - > svg { - margin-top: auto; - margin-bottom: auto; - } - } -} - -.mx_EventTile:hover .mx_HiddenMediaPlaceholder { - background-color: $background; -} diff --git a/apps/web/src/components/views/messages/HiddenMediaPlaceholder.tsx b/apps/web/src/components/views/messages/HiddenMediaPlaceholder.tsx deleted file mode 100644 index 16367ee05a..0000000000 --- a/apps/web/src/components/views/messages/HiddenMediaPlaceholder.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2025 New Vector 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 PropsWithChildren, type MouseEventHandler } from "react"; -import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -interface IProps { - onClick: MouseEventHandler; -} - -export const HiddenMediaPlaceholder: React.FunctionComponent> = ({ onClick, children }) => { - return ( - - ); -}; diff --git a/apps/web/src/components/views/messages/MImageBody.tsx b/apps/web/src/components/views/messages/MImageBody.tsx index 2d1b1402e5..351a40e000 100644 --- a/apps/web/src/components/views/messages/MImageBody.tsx +++ b/apps/web/src/components/views/messages/MImageBody.tsx @@ -16,6 +16,7 @@ import { ClientEvent } from "matrix-js-sdk/src/matrix"; import { type ImageContent } from "matrix-js-sdk/src/types"; import { Tooltip } from "@vector-im/compound-web"; import { ImageErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { HiddenMediaPlaceholder } from "@element-hq/web-shared-components"; import Modal from "../../../Modal"; import { _t } from "../../../languageHandler"; @@ -33,7 +34,6 @@ import { presentableTextForFile } from "../../../utils/FileUtils"; import { createReconnectedListener } from "../../../utils/connection"; import MediaProcessingError from "./shared/MediaProcessingError"; import { DecryptError, DownloadError } from "../../../utils/DecryptFile"; -import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder"; import { useMediaVisible } from "../../../hooks/useMediaVisible"; import { isMimeTypeAllowed } from "../../../utils/blobs.ts"; import { FileBodyFactory, renderMBody } from "./MBodyFactory"; diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.stories.tsx/default-auto.png new file mode 100644 index 0000000000..2a3e46c6ab Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index f1fbd18b54..04ac211dcb 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -18,6 +18,7 @@ export * from "./menus/UserMenu"; export * from "./room/timeline/ReadMarker"; export * from "./room/timeline/EventPresentation"; export * from "./room/timeline/event-tile/body/EventContentBodyView"; +export * from "./room/timeline/event-tile/body/HiddenMediaPlaceholder"; export * from "./room/timeline/event-tile/body/RedactedBodyView"; export * from "./room/timeline/event-tile/body/MFileBodyView"; export * from "./room/timeline/event-tile/body/MImageBodyView"; diff --git a/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.module.css b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.module.css new file mode 100644 index 0000000000..bad872ee38 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.module.css @@ -0,0 +1,36 @@ +/* + * 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. + */ + +.button { + border: none; + width: 100%; + height: 100%; + padding: var(--cpd-space-0x); + inset: var(--cpd-space-0x); + display: flex; + align-items: center; + justify-content: center; + text-align: center; + cursor: pointer; + background-color: var(--cpd-color-bg-subtle-secondary); +} + +.button:hover, +.button:focus-visible { + background-color: var(--cpd-color-bg-canvas-default); +} + +.content { + color: var(--cpd-color-text-action-accent); + display: inline-flex; + align-items: center; + gap: var(--cpd-space-1x); +} + +.icon { + flex: 0 0 auto; +} diff --git a/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.stories.tsx b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.stories.tsx new file mode 100644 index 0000000000..f30454aa4e --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.stories.tsx @@ -0,0 +1,38 @@ +/* + * 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 React from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { withViewDocs } from "../../../../../../.storybook/withViewDocs"; +import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder"; + +const HiddenMediaPlaceholderWrapper = withViewDocs(HiddenMediaPlaceholder, HiddenMediaPlaceholder); + +const meta = { + title: "Timeline/Timeline Body/HiddenMediaPlaceholder", + component: HiddenMediaPlaceholderWrapper, + tags: ["autodocs"], + args: { + children: "Show image", + onClick: fn(), + className: "", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.test.tsx b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.test.tsx new file mode 100644 index 0000000000..6122407ef8 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.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 userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder"; +import * as stories from "./HiddenMediaPlaceholder.stories"; + +const { Default } = composeStories(stories); + +describe("HiddenMediaPlaceholder", () => { + it("renders the default story", () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + expect(screen.getByRole("button", { name: "Show image" })).toBeInTheDocument(); + }); + + it("invokes the click handler", async () => { + const user = userEvent.setup(); + const onClick = vi.fn(); + + render(Show image); + + await user.click(screen.getByRole("button", { name: "Show image" })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it("applies a custom className to the root button", () => { + render( + + Show image + , + ); + + expect(screen.getByRole("button", { name: "Show image" })).toHaveClass("custom-hidden-media"); + }); +}); diff --git a/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.tsx b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.tsx new file mode 100644 index 0000000000..4aa3af9d54 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/HiddenMediaPlaceholder.tsx @@ -0,0 +1,41 @@ +/* + * 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 MouseEventHandler, type PropsWithChildren } from "react"; +import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; + +import styles from "./HiddenMediaPlaceholder.module.css"; + +export type HiddenMediaPlaceholderProps = PropsWithChildren<{ + /** + * CSS class names applied to the root button. + */ + className?: string; + /** + * Invoked when the user chooses to reveal the hidden media. + */ + onClick: MouseEventHandler; +}>; + +/** + * Renders a full-frame button used to reveal hidden media previews. + */ +export function HiddenMediaPlaceholder({ + className, + onClick, + children, +}: Readonly): JSX.Element { + return ( + + ); +} diff --git a/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/__snapshots__/HiddenMediaPlaceholder.test.tsx.snap b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/__snapshots__/HiddenMediaPlaceholder.test.tsx.snap new file mode 100644 index 0000000000..11ecde221d --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/__snapshots__/HiddenMediaPlaceholder.test.tsx.snap @@ -0,0 +1,35 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`HiddenMediaPlaceholder > renders the default story 1`] = ` +
+
+ +
+
+`; diff --git a/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/index.tsx b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/index.tsx new file mode 100644 index 0000000000..a332447ea6 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/body/HiddenMediaPlaceholder/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 { HiddenMediaPlaceholder, type HiddenMediaPlaceholderProps } from "./HiddenMediaPlaceholder";