import { PlusIcon, XIcon } from '@phosphor-icons/react' import type { Meta, StoryFn } from '@storybook/react' import React, { useEffect } from 'react' import { QueryChannelAPIResponse, StreamChat } from 'stream-chat' import { Channel, Chat, type SendButtonProps } from 'stream-chat-react' import { createMockStreamChatClient } from '../../testing/createMockStreamChatClient' import LockedAttachment from '../CustomMessage/LockedAttachment' import LinkAttachment from '../LinkAttachment' import { CustomMessageInput } from '.' // --------------------------------------------------------------------------- // Setup helpers (mirrors ChannelView.stories.tsx) // --------------------------------------------------------------------------- const mockUser = { id: 'storybook-user', name: 'Storybook User', image: 'https://i.pravatar.cc/150?img=1', } const mockParticipant = { id: 'participant-1', name: 'Jane Creator', image: 'https://i.pravatar.cc/150?img=5', } const createMockChannel = async ( client: StreamChat, opts: { frozen?: boolean } = {} ) => { const channelData = { members: [mockUser.id, mockParticipant.id], frozen: opts.frozen ?? false, } const ch = client.channel('messaging', 'storybook-cmi-channel', channelData) ch.watch = async () => { ch.state.members = { [mockUser.id]: { user: mockUser, user_id: mockUser.id }, [mockParticipant.id]: { user: mockParticipant, user_id: mockParticipant.id, }, } // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(ch as any)._data = channelData return { channel: channelData, members: [], messages: [], watchers: [], pinned_messages: [], duration: '0ms', } as unknown as QueryChannelAPIResponse } try { await ch.watch() } catch (_) { /* mock */ } // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(ch as any)._data = channelData return ch } // --------------------------------------------------------------------------- // Story wrapper — mounts the input inside a real Stream Channel context // --------------------------------------------------------------------------- type WrapperProps = { frozen?: boolean renderActions?: () => React.ReactNode sendButton?: React.ComponentType attachmentPreviewList?: React.ComponentType renderFooter?: () => React.ReactNode /** * Stands in for a host that publishes `--messaging-surface-height` — the * keyboard-shrunk viewport height the composer caps itself against. Set it to * pin the cap to a known value instead of leaving it viewport-relative. */ surfaceHeight?: number } const Wrapper: React.FC = (props) => { const { frozen, renderActions, sendButton, attachmentPreviewList, renderFooter, surfaceHeight, } = props const [client] = React.useState(() => createMockStreamChatClient(mockUser)) const [channel, setChannel] = React.useState | null>(null) useEffect(() => { createMockChannel(client, { frozen }).then(setChannel) }, [client, frozen]) if (!channel) return
Loading…
return (
) } // --------------------------------------------------------------------------- // Meta // --------------------------------------------------------------------------- const meta: Meta = { title: 'CustomMessageInput', component: CustomMessageInput, parameters: { layout: 'centered', }, } export default meta // --------------------------------------------------------------------------- // Stories // --------------------------------------------------------------------------- export const Default: StoryFn = (args) => Default.args = {} Default.parameters = { docs: { description: { story: 'Default message input. The send button (arrow) is disabled until the user types text — `useMessageComposerHasSendableData` controls this.', }, }, } export const Frozen: StoryFn = (args) => Frozen.args = { frozen: true } Frozen.parameters = { docs: { description: { story: 'Frozen channel state: the entire input area fades out (`aria-disabled`, `inert`), the textarea becomes read-only, and the send button is disabled.', }, }, } export const WithCustomActionButton: StoryFn = (args) => ( ) WithCustomActionButton.args = { renderActions: () => ( ), } WithCustomActionButton.parameters = { docs: { description: { story: 'Shows how `renderActions` injects extra controls (e.g. a media picker button) to the left of the message bubble.', }, }, } /** A custom send button that turns purple to signal a media attachment is staged. */ const PurpleMediaSendButton: React.FC = (props) => { const { sendMessage, disabled, ...rest } = props return ( ) } export const WithCustomSendButton: StoryFn = (args) => ( ) WithCustomSendButton.args = { sendButton: PurpleMediaSendButton } WithCustomSendButton.parameters = { docs: { description: { story: "Passes a custom `SendButton` via `Channel`'s `SendButton` prop (which is how `ChannelView`'s `sendButton` prop surfaces it). `CustomMessageInput` reads the button from `useComponentContext` and falls back to the default when none is provided.", }, }, } export const WithAttachments: StoryFn = (args) => ( ) WithAttachments.args = { attachmentPreviewList: () => (
Staged attachment
), renderActions: () => ( ), } WithAttachments.parameters = { docs: { description: { story: "Message input with a regular image attachment staged. In production thumbnails are portaled into `.central-container` by `ComposerPendingMediaPortal`; here we use `Channel`'s `AttachmentPreviewList` override to achieve the same visual placement via the React tree.", }, }, } export const WithStagedLinkPreviewUnderKeyboard: StoryFn = ( args ) => WithStagedLinkPreviewUnderKeyboard.args = { // Roughly a 393x852 phone with the soft keyboard up, so the 250px staged card // alone outgrows the composer's cap (half of this) and the card scrolls. surfaceHeight: 516, attachmentPreviewList: () => ( console.log('dismiss link preview')} /> ), } WithStagedLinkPreviewUnderKeyboard.parameters = { docs: { description: { story: 'MES-1037: with a link preview staged the composer used to grow tall enough for the soft keyboard to cover the textarea and send button. The white card is now capped at half `--messaging-surface-height` (the keyboard-shrunk height its host publishes) and scrolls as ONE region — the preview keeps its full height instead of being cropped to make room, and the send button sticks to the bottom of the scroll so a long message cannot carry it out of view.', }, }, } export const WithPaidAttachment: StoryFn = (args) => ( ) WithPaidAttachment.args = { attachmentPreviewList: () => ( console.log('dismiss paid attachment')} onEditClick={() => console.log('edit paid attachment')} /> ), renderActions: () => ( ), renderFooter: () => (
Tap to add a price
), } WithPaidAttachment.parameters = { docs: { description: { story: "Message input with a paid attachment staged inside the input bubble. Uses `Channel`'s `AttachmentPreviewList` override to render a `LockedAttachment isPreview` card at the top of the input. `onDismiss` removes it; `onEditClick` reopens the paid content wizard.", }, }, }