'use client'; import React, { useState, useCallback, useRef, useEffect } from 'react'; import { ChevronDown, ArrowDown } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import MessageBubble from './MessageBubble'; import MediaViewer from './MediaViewer'; import { isSameDay, format, isToday, isYesterday } from 'date-fns'; import { ptBR } from 'date-fns/locale'; import { normalizeJid, parseSafeDate } from '../utils/inboxUtils'; import { Message } from '../types/inboxTypes'; import { mediaApi } from '../services/chatApiService'; // Message interface removed and moved to types.ts interface MessageAreaProps { messages: Message[]; selectedChat: any; scrollRef: React.RefObject; onScroll: () => void; msgLoading: boolean; historyLoadingMore: boolean; hasMoreHistory: boolean; reactionPickerFor: string | null; onReply: (msg: Message) => void; onToggleReactionPicker: (msgId: string) => void; onReact: (msg: Message, emoji: string) => void; onForward?: (msg: Message) => void; onDelete?: (msg: Message) => void; onStar?: (msg: Message) => void; onEdit?: (msg: Message) => void; quickReactions: string[]; getGroupSenderLabel: (msg: Message) => string; } const MessageArea: React.FC = ({ messages, selectedChat, scrollRef, onScroll, msgLoading, historyLoadingMore, hasMoreHistory, reactionPickerFor, onReply, onToggleReactionPicker, onReact, onForward, onDelete, onStar, onEdit, quickReactions, getGroupSenderLabel, }) => { const [showScrollBottom, setShowScrollBottom] = useState(false); const lastScrollTop = useRef(0); const needsInitialScroll = useRef(false); // ── Media viewer state ──────────────────────────────────────────────────── const [viewerMsgId, setViewerMsgId] = useState(null) // ── Re-download de mídia não baixada ───────────────────────────────────── const handleRedownloadMedia = useCallback(async (msg: Message) => { // selectedChat no formato legado usa instance_name (não instanceId) const instanceId = selectedChat?.instance_name ?? selectedChat?.instanceId if (!instanceId || !msg.id) return await mediaApi.redownload(instanceId, String(msg.id)) }, [selectedChat?.instance_name, selectedChat?.instanceId]) const mediaMessages = messages.filter(m => m.type === 'image' || m.type === 'video') const handleInternalScroll = useCallback(() => { const el = scrollRef.current; if (!el) return; const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 100; setShowScrollBottom(!isAtBottom); // Trigger parent onScroll (for infinite loading etc) onScroll(); lastScrollTop.current = el.scrollTop; }, [onScroll, scrollRef]); const scrollToBottom = (smooth = true) => { const el = scrollRef.current; if (el) { el.scrollTo({ top: el.scrollHeight, behavior: smooth ? 'smooth' : 'instant' }); } }; const formatDateSeparator = (date: Date) => { try { if (isToday(date)) return 'Hoje'; if (isYesterday(date)) return 'Ontem'; return format(date, "d 'de' MMMM 'de' yyyy", { locale: ptBR }); } catch (e) { return 'Histórico'; } }; const isGroupChat = Boolean(selectedChat?.remote_jid?.endsWith('@g.us')); const chatId = selectedChat?.remote_jid ?? null; // Marca que precisa scroll ao fundo quando trocar de chat useEffect(() => { needsInitialScroll.current = true; }, [chatId]); // Auto-scroll: roda sempre que messages muda useEffect(() => { const el = scrollRef.current; if (!el || messages.length === 0) return; if (needsInitialScroll.current) { // Carga inicial após selectChat → scroll instantâneo ao fundo needsInitialScroll.current = false; // Triplo rAF: aguarda React render + layout + paint requestAnimationFrame(() => { requestAnimationFrame(() => { requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; }); }); }); } else { // Nova mensagem em tempo real → só rola se já estiver perto do fundo const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 250; if (isNearBottom) { requestAnimationFrame(() => { el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); }); } } }, [messages, scrollRef]); return ( <>
{/* Loader for history */} {historyLoadingMore && (
)} {(() => { const seen = new Set(); return messages .filter(m => { // Reactions não são mensagens visuais — são metadados if (m.type === 'reaction') return false; const key = m.message_id || m.id?.toString(); if (!key || seen.has(key)) return false; seen.add(key); return true; }) .map((msg, idx) => { const msgDate = parseSafeDate(msg.timestamp); const prevMsg = messages[idx - 1]; const prevDate = prevMsg ? parseSafeDate(prevMsg.timestamp) : null; const showDateSeparator = !prevDate || !isSameDay(prevDate, msgDate); // Authentic WA Tail Logic: Show tail on the first message of a sequence from the same sender const isSameSenderAsPrev = prevMsg && (prevMsg.direction === msg.direction); const showTail = showDateSeparator || !isSameSenderAsPrev; const senderLabel = isGroupChat ? getGroupSenderLabel(msg) : ''; // Resolve Quoted Sender let quotedSenderLabel = 'Mensagem'; if (msg.quoted_participant) { const qp = normalizeJid(msg.quoted_participant); const chatJid = normalizeJid(selectedChat?.remote_jid || ''); if (qp === chatJid) quotedSenderLabel = selectedChat?.subject || 'Contato'; else quotedSenderLabel = 'Você'; } return ( {showDateSeparator && (
{formatDateSeparator(msgDate)}
)} setViewerMsgId(m.message_id)} onRedownloadMedia={handleRedownloadMedia} />
); }); })()} {/* Bottom Anchor */}
{/* Scroll to Bottom Button */} {showScrollBottom && ( scrollToBottom()} className="absolute bottom-6 right-6 w-11 h-11 bg-white text-[#54656f] rounded-full shadow-lg flex items-center justify-center hover:bg-[#f0f2f5] transition-all z-30 border border-[#d1d7db] active:scale-90" > {selectedChat?.unread_count > 0 && ( {selectedChat.unread_count} )} )} {/* Initial Loading Overlay */} {msgLoading && messages.length === 0 && (
Carregando mensagens...
)}
{/* ── Media viewer portal ───────────────────────────────────────────── */} {viewerMsgId && mediaMessages.length > 0 && ( setViewerMsgId(null)} onReply={onReply} onStar={onStar} /> )} ); }; export default MessageArea;