import React, { useContext } from 'react' import type { Event, UserResponse } from 'stream-chat' import { AIStates, useAIState, useChannelStateContext, useChatContext, useTypingContext, } from 'stream-chat-react' import { Avatar } from '../Avatar' import { MessageTail } from '../CustomMessage/MessageTail' import { DmAgentEnabledContext } from './DmAgentContext' interface CustomTypingIndicatorProps { threadList?: boolean } const Circle = ({ cx, index }: { cx: string; index: number }) => ( ) const AI_ACTIVE_STATES = new Set([ AIStates.Thinking, AIStates.Generating, AIStates.ExternalSources, ]) const CustomTypingIndicator = ({ threadList }: CustomTypingIndicatorProps) => { const { channel, channelConfig, thread } = useChannelStateContext() const { client } = useChatContext() const { typing = {} } = useTypingContext() const { aiState } = useAIState(channel) const dmAgentEnabled = useContext(DmAgentEnabledContext) // Show the AI indicator whenever the consumer agent is producing a reply. // This event stream is independent of `typing.start`/`typing.stop`, so it is // intentionally NOT gated by `channelConfig.typing_events`. Gate strictly on // `dmAgentEnabled` so stale or off-surface ai_indicator events never surface // the bubble on channels where the agent is not active. const isAiActive = !threadList && dmAgentEnabled && AI_ACTIVE_STATES.has(aiState) if (isAiActive) { const agentUser = findOtherChannelUser(channel, client.user?.id) return ( ) } if (channelConfig?.typing_events === false) { return null } const typingInChannel = !threadList ? Object.values(typing).filter( ({ parent_id, user }: Event) => user?.id !== client.user?.id && !parent_id ) : [] const typingInThread = threadList ? Object.values(typing).filter( ({ parent_id, user }: Event) => user?.id !== client.user?.id && parent_id === thread?.id ) : [] const typingUsers = threadList ? typingInThread : typingInChannel if (!typingUsers.length) { return null } const typingUser = typingUsers[0]?.user const memberUser = typingUser?.id && channel.state.members[typingUser.id] ? channel.state.members[typingUser.id].user : undefined return ( ) } const TypingBubble = ({ avatarId, avatarName, avatarImage, testId, }: { avatarId: string avatarName: string avatarImage?: string | null testId: string }) => (
) type ChannelLike = ReturnType['channel'] function findOtherChannelUser( channel: ChannelLike, selfId: string | undefined ): UserResponse | undefined { const members = channel?.state?.members ?? {} for (const member of Object.values(members)) { const memberUser = member?.user if (memberUser && memberUser.id !== selfId) { return memberUser } } return undefined } export default CustomTypingIndicator