import type { Meta, StoryFn } from '@storybook/react'
import React, { useState } from 'react'
import { MessageText } from 'stream-chat-react'
import type { LockedAttachmentGalleryItem } from './types'
import LockedAttachment from '.'
const meta: Meta = {
title: 'LockedAttachment',
parameters: { layout: 'fullscreen' },
}
export default meta
const MatrixTable: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
Visitor
|
Creator
|
{children}
)
const MatrixTableRow: React.FC<{
label: React.ReactNode
received: React.ReactNode
sent: React.ReactNode
}> = ({ label, received, sent }) => (
|
{label}
|
{received}
|
{sent}
|
)
/**
* Deliberately renders WITHOUT any `.str-chat` ancestor or
* `str-chat__message*` structure — the real Composer/preview surfaces this
* card renders in outside a message thread. If a card looks wrong here, it's
* leaning on ambient stream-chat-react CSS instead of its own explicit
* styling.
*/
const MatrixTableCell: React.FC<{
isMine: boolean
children: React.ReactNode
}> = ({ children }) => {children}
export const Text: StoryFn = () => {
const [isUnlocking, setUnlocking] = useState(false)
// Matches what `CustomMessage` passes `MessageText` for a revealed paid
// text attachment (`displayMessage`) — just enough of a `LocalMessage`
// shape to render the real `str-chat__message-text` markup/CSS.
const purchasedMessage = {
text: "Here's the address, see you Thursday at 7!",
type: 'regular',
} as Parameters[0]['message']
return (
alert('Removed')}
/>
}
/>
}
/>
{
setUnlocking(true)
setTimeout(() => setUnlocking(false), 1500)
}}
/>
}
sent={
}
/>
}
sent={null}
/>
}
/>
}
sent={
}
/>
}
/>
)
}
const VIDEO_THUMBNAIL_BLURRED = '/video-thumbnail-blurred.jpg'
const VIDEO_THUMBNAIL = '/video-thumbnail.jpg'
const VIDEO_SOURCE = '/video-source.mp4'
const AUDIO_THUMBNAIL_BLURRED = '/audio-thumbnail-blurred.jpg'
const AUDIO_THUMBNAIL = '/audio-thumbnail.jpg'
const AUDIO_SOURCE = '/audio-source.mp3'
const IMAGE_THUMBNAIL_BLURRED = '/image-thumbnail-blurred.jpg'
const IMAGE_THUMBNAIL = '/image-thumbnail.jpg'
const IMAGE_SOURCE = '/image-source.jpg'
const DOCUMENT_THUMBNAIL_BLURRED = '/document-thumbnail-blurred.jpg'
const DOCUMENT_THUMBNAIL = '/document-thumbnail.jpg'
const DOCUMENT_SOURCE = '/document-source.pdf'
const VARIANTS = [
{
label: 'Video',
title: "Alicia's Workout Plan",
mimeType: 'video/mp4',
detail: '1:20',
thumbnailUrl: VIDEO_THUMBNAIL_BLURRED,
thumbnailUnlockedUrl: VIDEO_THUMBNAIL,
sourceUrl: VIDEO_SOURCE,
},
{
label: 'Audio',
title: 'Morning Meditation',
mimeType: 'audio/mpeg',
detail: '4:35',
thumbnailUrl: AUDIO_THUMBNAIL_BLURRED,
thumbnailUnlockedUrl: AUDIO_THUMBNAIL,
sourceUrl: AUDIO_SOURCE,
},
{
label: 'Image',
title: 'Picture of my cat',
mimeType: 'image/jpeg',
detail: '3.2 MB',
thumbnailUrl: IMAGE_THUMBNAIL_BLURRED,
thumbnailUnlockedUrl: IMAGE_THUMBNAIL,
sourceUrl: IMAGE_SOURCE,
},
{
label: 'Document',
title: 'Strength Training Guide',
mimeType: 'application/zip',
detail: '14 files',
thumbnailUrl: DOCUMENT_THUMBNAIL_BLURRED,
thumbnailUnlockedUrl: DOCUMENT_THUMBNAIL,
sourceUrl: DOCUMENT_SOURCE,
},
{
label: 'Unknown',
title: 'Unknown Attachment',
mimeType: 'application/octet-stream',
detail: '2.4 MB',
thumbnailUrl: undefined,
thumbnailUnlockedUrl: undefined,
sourceUrl: DOCUMENT_SOURCE,
},
]
/**
* A mixed-media collection (a video + 3 photos) used by the Gallery stories
* below. `LockedAttachment` accepts a `gallery` prop and renders a carousel
* of these items when 2+ items are provided.
*/
const GALLERY_ITEMS: LockedAttachmentGalleryItem[] = [
{
mimeType: 'video/mp4',
thumbnailUrl: VIDEO_THUMBNAIL,
sourceUrl: VIDEO_SOURCE,
},
{
mimeType: 'image/jpeg',
thumbnailUrl: IMAGE_THUMBNAIL,
sourceUrl: IMAGE_SOURCE,
},
{
mimeType: 'application/pdf',
thumbnailUrl: DOCUMENT_THUMBNAIL,
sourceUrl: DOCUMENT_SOURCE,
},
{
mimeType: 'audio/mpeg',
thumbnailUrl: AUDIO_THUMBNAIL,
sourceUrl: AUDIO_SOURCE,
},
]
const GALLERY_DETAIL = '4 items'
const GALLERY_TITLE = 'Workout Guide'
const MediaMatrix: React.FC<{ variant: (typeof VARIANTS)[number] }> = ({
variant,
}) => {
const [isPaid, setPaid] = useState(false)
const [isUnlocking, setUnlocking] = useState(false)
const {
title,
mimeType,
detail,
thumbnailUrl,
thumbnailUnlockedUrl,
sourceUrl,
} = variant
const fetchSource = async () => ({
sourceUrl,
thumbnailUrl: thumbnailUnlockedUrl,
})
return (
alert('Dismissed')}
/>
}
/>
}
/>
{
setUnlocking(true)
setTimeout(() => {
setUnlocking(false)
setPaid(true)
}, 1500)
}}
onDownloadClick={() => alert('Download clicked')}
onFetchSource={fetchSource}
/>
}
sent={
}
/>
alert('Download clicked')}
onFetchSource={fetchSource}
/>
}
// Sender never unlocks their own message — no distinct state to show.
sent={null}
/>
alert('Unlock clicked')}
onDownloadClick={() => alert('Download clicked')}
onFetchSource={fetchSource}
/>
}
sent={
}
/>
)
}
const variantByLabel = (label: string) =>
VARIANTS.find((v) => v.label === label)!
export const Video: StoryFn = () => (
)
// LockedAttachment (contentType="media", !isMine) autoplays the video preview once paid; even with
// our SMIL/CSS animation freeze the