import type { Meta, StoryFn } from '@storybook/react'
import classNames from 'classnames'
import React from 'react'
import type { Attachment, LocalMessage } from 'stream-chat'
import {
currentUserArgType,
storyUsers,
type StoryUser,
} from '../../stories/decorators/storyUser'
import StreamAttachmentMessage from './StreamAttachmentMessage'
/* ──────────────────────────────────────────────────────────────────
* Stories for `StreamAttachmentMessage` — the toolkit-owned renderer
* that `CustomMessage` uses for every generic Stream-attachment message
* (link OG previews + image / video / audio / pdf / file media).
*
* Unlike `MessageAttachmentConversations.stories.tsx` — which hand-builds
* `MessageAttachment.*` JSX directly — these stories feed raw Stream
* `Attachment[]` shapes through `StreamAttachmentMessage` so they exercise
* the mapping the component actually owns: ordered segment building,
* Stream-attachment → `LinkAttachment` prop mapping, caption
* de-duplication (including emoji preservation), and media clustering.
*
* The component owns the Stream bubble-wrapper markup. Stories only supply
* the `.str-chat` / `.str-chat__message--me|other` / `.str-chat__li--single`
* ancestry that Stream bubble chrome and the MES-1031 tails are scoped under.
* ────────────────────────────────────────────────────────────────── */
const Bubbles: React.FC<{
isMyMessage: boolean
children: React.ReactNode
}> = ({ isMyMessage, children }) => (
{/* Tail CSS is scoped to `.str-chat__li--single|--bottom`; bubble surface
styles need `.str-chat__message--me|other`. */}
)
const Render = (attachments: Attachment[], text?: string): StoryFn =>
function StreamAttachmentStory({ currentUser }) {
const isMyMessage = currentUser.id === storyUsers.creator.id
return (
)
}
const HERO = 'https://picsum.photos/seed/og-hero/1200/630'
const PHOTO = (seed: string) => `https://picsum.photos/seed/${seed}/720/720`
const VIDEO_SRC = '/video-source.mp4'
const VIDEO_POSTER = '/video-thumbnail.jpg'
const PDF_SRC = 'https://www.w3.org/WAI/WCAG21/wcag21.pdf'
const linkAttachment = (overrides: Partial = {}): Attachment => ({
type: 'link',
og_scrape_url: 'https://linktr.ee/marketplace',
title: 'Linktree Marketplace',
text: 'Browse the best apps to customize your link in bio.',
image_url: HERO,
...overrides,
})
const imageAttachment = (seed: string): Attachment => ({
type: 'image',
image_url: PHOTO(seed),
})
interface StoryArgs {
currentUser: StoryUser
}
const meta: Meta = {
title: 'CustomMessage/StreamAttachmentMessage',
argTypes: currentUserArgType,
args: { currentUser: storyUsers.creator },
parameters: {
layout: 'fullscreen',
// Story render output embeds live React elements; pin source generation
// to the static story code (matches MessageAttachmentConversations).
docs: { source: { type: 'code' } },
},
}
export default meta
/** Link share with an OG image → the featured card layout. */
export const LinkFeatured = Render([linkAttachment()])
/** Link share with no thumbnail → the classic (compact) card layout. */
export const LinkClassic = Render([
linkAttachment({ image_url: undefined, thumb_url: undefined }),
])
/**
* Link share with an emoji-only caption alongside the URL. The emoji is
* real content, so the caption is preserved as a separate bubble beneath
* the card (regression guard for the caption-dedup predicate).
*/
export const LinkWithEmojiCaption = Render(
[linkAttachment({ og_scrape_url: 'https://mrdrnose.itch.io/votv' })],
'https://mrdrnose.itch.io/votv 👽'
)
/**
* Caption that is just the link URL → suppressed (the card already shows
* the URL, so no duplicate text bubble).
*/
export const LinkWithUrlOnlyCaption = Render(
[linkAttachment()],
'https://linktr.ee/marketplace'
)
/** Single image attachment. */
export const SingleImage = Render([imageAttachment('single')])
/** Several images in one message → a clustered image bubble. */
export const ImageCluster = Render([
imageAttachment('cluster-1'),
imageAttachment('cluster-2'),
imageAttachment('cluster-3'),
])
/** Video attachment (poster + play affordance). */
export const Video = Render([
{
type: 'video',
asset_url: VIDEO_SRC,
thumb_url: VIDEO_POSTER,
mime_type: 'video/mp4',
title: 'trailer.mp4',
},
])
/** PDF document attachment with a caption. */
export const Pdf = Render(
[
{
type: 'file',
asset_url: PDF_SRC,
mime_type: 'application/pdf',
title: 'launch-checklist.pdf',
file_size: 142_336,
},
],
'Section 4 has the details.'
)
/** Generic (non-PDF) file attachment with a download affordance. */
export const GenericFile = Render([
{
type: 'file',
asset_url: 'https://cdn.example.com/brand-kit.zip',
mime_type: 'application/zip',
title: 'brand-kit.zip',
file_size: 11_500_000,
},
])
/**
* Mixed message — media followed by a link share with a caption. Exercises
* ordered-segment building across kinds: the image cluster renders first,
* the link card second, and the caption attaches to the last bubble.
*/
export const MixedMediaAndLink = Render(
[imageAttachment('mixed-1'), imageAttachment('mixed-2'), linkAttachment()],
'Check these out 🔥'
)