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