import type { Meta, StoryFn } from '@storybook/react' import React from 'react' import type { Event } from 'stream-chat' import { AIStates, ChannelStateProvider, ChatProvider, TypingProvider, } from 'stream-chat-react' import { DmAgentEnabledContext } from './DmAgentContext' import CustomTypingIndicator from '.' type StoryProps = { typingEventsEnabled?: boolean typing?: Record aiState?: string dmAgentEnabled?: boolean } const currentUser = { id: 'visitor-1', name: 'Visitor', } const typingUser = { id: 'creator-1', name: 'Creator', image: 'https://i.pravatar.cc/48?img=5', } const defaultTyping: Record = { [typingUser.id]: { type: 'typing.start', user: typingUser, parent_id: undefined, } as Event, } type ListenerMap = Record< string, (event: { ai_state: string; cid: string }) => void > const createMockChannel = ({ aiState }: { aiState?: string }) => { const listeners: ListenerMap = {} const channel = { cid: 'messaging:test', state: { members: { [currentUser.id]: { user: currentUser }, [typingUser.id]: { user: typingUser }, }, }, on(eventType: string, handler: ListenerMap[string]) { listeners[eventType] = handler // Fire the requested AI state synchronously so the hook picks it up. if (eventType === 'ai_indicator.update' && aiState) { handler({ ai_state: aiState, cid: 'messaging:test' }) } return { unsubscribe: () => {} } }, } return channel } const StoryWrapper: React.FC = ({ typingEventsEnabled = true, typing = {}, aiState, dmAgentEnabled = true, }) => { const channel = createMockChannel({ aiState }) const chatContextValue = { client: { user: currentUser, }, theme: 'str-chat__theme-light', channelsQueryState: { error: null, queryInProgress: null, }, latestMessageDatesByChannels: {}, mutes: [], closeMobileNav: () => {}, openMobileNav: () => {}, getAppSettings: () => null, setActiveChannel: () => {}, themeVersion: '2', useImageFlagEmojisOnWindows: false, } const channelStateValue = { channel, channelCapabilities: {}, channelConfig: { typing_events: typingEventsEnabled, }, imageAttachmentSizeHandler: () => ({}), multipleUploads: false, notifications: [], shouldGenerateVideoThumbnail: false, suppressAutoscroll: false, videoAttachmentSizeHandler: () => ({}), } return (
) } const meta: Meta = { title: 'CustomTypingIndicator', component: CustomTypingIndicator, parameters: { layout: 'centered', // CustomTypingIndicator is animation-dominant (SMIL spinner + DOM // appear/disappear on typing-state transitions). Even with the SMIL // pause decorator, a snapshot here is mostly testing what frame the // animation paused at — low value for visual regression, high flake // potential. Skip it in Chromatic; the stories stay browsable in // Storybook for design review. chromatic: { disableSnapshot: true }, }, } export default meta export const Default: StoryFn = (args) => Default.args = { typing: defaultTyping, } export const HiddenWhenNoTyping: StoryFn = (args) => ( ) HiddenWhenNoTyping.args = { typing: {}, } export const AiAgentThinking: StoryFn = (args) => ( ) AiAgentThinking.args = { aiState: AIStates.Thinking, } export const AiAgentGenerating: StoryFn = (args) => ( ) AiAgentGenerating.args = { aiState: AIStates.Generating, }