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`. */}
{children}
) 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 🔥' )