"use client" import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, } from "react" import { ConversationMessageBubble } from "@/components/chat/conversation-message-bubble" import { ConversationMessageRow } from "@/components/chat/conversation-message-row" import { ConversationMessageScroller, ConversationMessageScrollerItem, type ConversationMessageScrollerHandle, } from "@/components/chat/conversation-message-scroller" import { ImMessageHTML } from "@/components/im-message-html" import { useImageLightbox } from "@/components/image-lightbox" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import type { ImMessage } from "@/lib/api/im" import { renderIMMessageHTML } from "@/lib/im-message" import { cn, formatDateTime } from "@/lib/utils" import { useI18n } from "@/i18n/provider" type SupportChatMessageListProps = { messages?: ImMessage[] | null onNearBottomVisible?: () => void hasMoreOlder?: boolean loadingOlder?: boolean onLoadOlder?: () => Promise } export type SupportChatMessageListHandle = { scrollToBottom: () => void } function getDayKey(value?: string) { if (!value) { return "unknown" } const date = new Date(value) if (Number.isNaN(date.getTime())) { return value.slice(0, 10) } return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String( date.getDate() ).padStart(2, "0")}` } function getTimelineLabel( value: string | undefined, t: (key: string, values?: Record) => string ) { if (!value) { return t("supportChat.justNow") } const date = new Date(value) if (Number.isNaN(date.getTime())) { return value } const currentDayKey = getDayKey(value) const todayDayKey = getDayKey(new Date().toISOString()) const timeText = `${String(date.getHours()).padStart(2, "0")}:${String( date.getMinutes() ).padStart(2, "0")}` if (currentDayKey === todayDayKey) { return t("supportChat.todayAt", { time: timeText }) } return `${currentDayKey} ${timeText}` } export const SupportChatMessageList = forwardRef( function SupportChatMessageList( { messages, onNearBottomVisible, hasMoreOlder = false, loadingOlder = false, onLoadOlder, }, ref ) { const t = useI18n() const scrollerRef = useRef(null) const shouldStickToBottomRef = useRef(true) const onNearBottomVisibleRef = useRef(onNearBottomVisible) const safeMessages = Array.isArray(messages) ? messages : [] useEffect(() => { onNearBottomVisibleRef.current = onNearBottomVisible }, [onNearBottomVisible]) const scrollToBottom = useCallback(() => { scrollerRef.current?.scrollToBottom() }, []) const handleImageSettled = useCallback(() => { if (shouldStickToBottomRef.current) { scrollToBottom() onNearBottomVisibleRef.current?.() } }, [scrollToBottom]) useImperativeHandle(ref, () => ({ scrollToBottom, })) const handleLoadOlder = useCallback(async () => { if (!onLoadOlder || loadingOlder || !hasMoreOlder) { return } await onLoadOlder() }, [hasMoreOlder, loadingOlder, onLoadOlder]) return ( { shouldStickToBottomRef.current = nearBottom }} onNearBottomVisible={onNearBottomVisible} topSlot={ hasMoreOlder && onLoadOlder ? (
) : null } > {safeMessages.length === 0 ? (
{t("supportChat.emptyPrompt")}
) : null} {safeMessages.map((message, index) => { const previousMessage = index > 0 ? safeMessages[index - 1] : null const showTimeline = index === 0 || getDayKey(previousMessage?.sentAt) !== getDayKey(message.sentAt) return ( ) })}
) } ) type MessageItemProps = { message: ImMessage showTimeline: boolean onImageSettled: () => void timelineLabel: string } const MessageItem = memo( function MessageItem({ message, showTimeline, onImageSettled, timelineLabel }: MessageItemProps) { const t = useI18n() const { open } = useImageLightbox() const isCustomer = message.senderType === "customer" const senderName = isCustomer ? t("supportChat.customerSelf") : message.senderName?.trim() || t("supportChat.agentLabel") const avatarSrc = !isCustomer && message.senderAvatar?.trim() ? message.senderAvatar.trim() : undefined const htmlContent = renderIMMessageHTML(message) const fallbackName = senderName.slice(0, 1).toUpperCase() return (
{showTimeline ? (
{timelineLabel}
) : null} {avatarSrc ? : null} {fallbackName || t("supportChat.customerFallback")} ) : null } contentClassName="max-w-[86%] gap-1.5" headerClassName="flex-wrap gap-x-2 gap-y-1 px-1 text-[11px]" header={ <> {senderName} {formatDateTime(message.sentAt)} {isCustomer ? ( {message.agentRead ? t("supportChat.agentRead") : t("supportChat.agentUnread")} ) : null} } >
) }, (prevProps, nextProps) => isSameMessageItemRender(prevProps.message, nextProps.message) && prevProps.showTimeline === nextProps.showTimeline && prevProps.timelineLabel === nextProps.timelineLabel && prevProps.onImageSettled === nextProps.onImageSettled ) function isSameMessageItemRender(prev: ImMessage, next: ImMessage) { return ( prev.id === next.id && prev.senderType === next.senderType && prev.senderName === next.senderName && prev.senderAvatar === next.senderAvatar && prev.messageType === next.messageType && prev.content === next.content && prev.payload === next.payload && prev.sentAt === next.sentAt && prev.agentRead === next.agentRead ) }