import classNames from 'classnames'
import React, { useContext, useMemo, useState } from 'react'
import {
Attachment as DefaultAttachment,
EditMessageModal as DefaultEditMessageModal,
MessageBounceModal,
MessageBouncePrompt as DefaultMessageBouncePrompt,
MessageBlocked as DefaultMessageBlocked,
MessageDeleted as DefaultMessageDeleted,
MessageErrorIcon,
MessageIsThreadReplyInChannelButtonIndicator as DefaultMessageIsThreadReplyInChannelButtonIndicator,
MessageRepliesCountButton as DefaultMessageRepliesCountButton,
MessageText,
Poll,
ReminderNotification as DefaultReminderNotification,
StreamedMessageText as DefaultStreamedMessageText,
areMessageUIPropsEqual,
isDateSeparatorMessage,
isMessageBlocked,
isMessageBounced,
messageHasAttachments,
messageHasReactions,
useChannelStateContext,
useComponentContext,
useChatContext,
useMessageContext,
useMessageReminder,
type MessageContextValue,
type MessageUIComponentProps,
} from 'stream-chat-react'
import type { OutboundClickHandler, ResolvedOutbound } from '../../types'
import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
import { Avatar } from '../Avatar'
import { isLinkAttachment } from '../MediaMessage'
import type { BubbleGroupPosition } from '../MessageAttachment/types'
import { useCustomMessage } from './context'
import LockedAttachment from './LockedAttachment'
import {
MessageTag,
isAttachmentMessage,
isChatbotMessage,
isMediaAttachmentMessage,
isTextAttachmentMessage,
} from './MessageTag'
import { MessageTail } from './MessageTail'
import { OutboundContext } from './OutboundContext'
import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
import StreamAttachmentMessage, {
buildOrderedAttachmentSegments,
} from './StreamAttachmentMessage'
import { TipMessage, isTipMessage } from './TipMessage'
type CustomMessageUIComponentProps = MessageUIComponentProps & {
viewerLanguage?: string
onOutboundClick?: OutboundClickHandler
}
type CustomMessageWithContextProps = MessageContextValue & {
viewerLanguage?: string
onOutboundClick?: OutboundClickHandler
resolvedOutbound?: ResolvedOutbound
}
const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
const {
additionalMessageInputProps,
editing,
endOfGroup,
firstOfGroup,
groupStyles,
groupedByUser,
handleAction,
handleOpenThread,
handleRetry,
highlighted,
isMessageAIGenerated,
isMyMessage,
message,
renderText,
threadList,
viewerLanguage,
onOutboundClick,
resolvedOutbound,
} = props
const { client } = useChatContext('CustomMessage')
const { channel } = useChannelStateContext('CustomMessage')
const { isUnlocking, onUnlockClick, onFetchSource, onDownloadClick } =
useCustomMessage('LockedAttachment')
const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
const reminder = useMessageReminder(message.id)
const {
Attachment = DefaultAttachment,
EditMessageModal = DefaultEditMessageModal,
MessageActions,
MessageBlocked = DefaultMessageBlocked,
MessageBouncePrompt = DefaultMessageBouncePrompt,
MessageDeleted = DefaultMessageDeleted,
MessageIsThreadReplyInChannelButtonIndicator = DefaultMessageIsThreadReplyInChannelButtonIndicator,
MessageRepliesCountButton = DefaultMessageRepliesCountButton,
ReminderNotification = DefaultReminderNotification,
StreamedMessageText = DefaultStreamedMessageText,
PinIndicator,
} = useComponentContext('CustomMessage')
const hasAttachment = messageHasAttachments(message)
const hasReactions = messageHasReactions(message)
const isAIGenerated = useMemo(
() => isMessageAIGenerated?.(message),
[isMessageAIGenerated, message]
)
const finalAttachments = useMemo(() => {
const attachments = message.attachments ?? []
const raw = message.shared_location
? [message.shared_location, ...attachments]
: attachments
if (!isChatbotMessage(message)) return raw
const filtered = raw.filter((a) => !('type' in a) || !isLinkAttachment(a))
return filtered.length === raw.length ? raw : filtered
}, [message])
const attachmentSegments = useMemo(
() => buildOrderedAttachmentSegments(finalAttachments),
[finalAttachments]
)
const displayMessage = useMemo(() => {
const displayText = getMessageDisplayText({ message, viewerLanguage })
return displayText === message.text
? message
: { ...message, text: displayText }
}, [message, viewerLanguage])
// Route every generic Stream-attachment message (link OG previews and
// image/video/audio/pdf/file media) through the toolkit renderer, so
// messaging-react owns all shared attachment rendering rather than leaving
// media on stream-chat-react's default Attachment. App-specific types
// (locked/paid, tips, chatbot) are handled by the branches above.
const hasAttachmentSegments = attachmentSegments.length > 0
// Only own the message when every attachment maps to a representable segment.
// Otherwise a mixed message (e.g. an unsupported shared_location alongside a
// photo) would silently drop the unrepresented attachment — those fall
// through to the default `Attachment` renderer, which handles all of them.
const allAttachmentsRepresented =
attachmentSegments.reduce(
(total, segment) => total + segment.attachments.length,
0
) === (finalAttachments?.length ?? 0)
// Stream's MessageList sets the group-style string that drives the
// `.str-chat__li--{single,top,middle,bottom}` class; the `firstOfGroup` /
// `endOfGroup` / `groupedByUser` booleans it forwards are unset in this render
// path (see `renderMessages`), so read the style directly.
const groupStyle = groupStyles?.[0] ?? ''
const bubbleGroupPosition: BubbleGroupPosition =
groupStyle === 'top'
? 'first'
: groupStyle === 'middle'
? 'middle'
: groupStyle === 'bottom'
? 'end'
: 'single'
// The trailing bubble of a same-author run carries the tail — matches the
// `.str-chat__li--single` / `--bottom` the old `::after` keyed off.
const showTail = groupStyle === 'single' || groupStyle === 'bottom'
if (isDateSeparatorMessage(message)) {
return null
}
if (message.deleted_at || message.type === 'deleted') {
return