import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Search, MoreVertical, User, MessageSquare, ChevronDown, RefreshCw, MessageSquarePlus, RotateCw, ArrowLeft, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { format } from 'date-fns'; import { ptBR } from 'date-fns/locale'; import type { NewWhatsChat } from './types/inboxTypes'; import { getAvatarProxyUrl, formatPhone } from './utils/inboxUtils'; import { useNewInboxBridge } from './hooks/inbox/useNewInboxBridge'; import { useInstanceSelector } from './hooks/inbox/useInstanceSelector'; import { useChatFilter } from './hooks/inbox/useChatFilter'; import { useChatWindow } from './hooks/inbox/useChatWindow'; import { useMessageInput } from './hooks/inbox/useMessageInput'; import { nw } from './services/nwClient'; import { useProtocolPanel } from './hooks/useProtocolPanel'; import ChatSidebar from './components/ChatSidebar'; import ThinSidebar, { TabType } from './components/ThinSidebar'; import InboxHeader from './components/InboxHeader'; import MessageArea from './components/MessageArea'; import SettingsPanel from './components/SettingsPanel'; import InputBar from './components/InputBar'; import NewConversationModal from './components/NewConversationModal'; import ContactProfile from './components/ContactProfile'; import ProtocolPanel from './components/ProtocolPanel'; import { HybridBackend } from '../../services/backend'; type Chat = NewWhatsChat; export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void }) { // ── Seleção de instância (hook dedicado) ───────────────────────────────── const { instances, activeInstance, mappedActiveInstance, connectedInstances, isConnected, dropdownRef, selectInstance, refreshInstances, handleConnect: handleConnectInstance, } = useInstanceSelector() // ── Inbox bridge (chats + mensagens) ────────────────────────────────────── const { chats: chatsStore, selectedChat, messages, loadingChats: loading, loadingMessages, presenceByJid, handleSelectChat: handleSelectChatBridge, handleSendText, handleLoadMoreHistory, handleMenuAction, } = useNewInboxBridge() // ── Filtros de chat (hook dedicado) ─────────────────────────────────────── const { searchTerm, activeFilter, chatScope, chatsByScope, isFavoriteChat, setSearchTerm, setActiveFilter, setChatScope, toggleFavorite, } = useChatFilter(chatsStore) // ── Chat window (hook dedicado) ─────────────────────────────────────────── const { scrollRef, isTyping, replyingTo, reactionPickerFor, hasMoreHistory, loadingMore, isProfileOpen, isSyncingAvatar, isChatMenuOpen, setReplyingTo, setIsProfileOpen, setIsChatMenuOpen, handleTyping, handleScroll, handleSelectChat: onChatWindowSelect, handleReactToMessage, handleSendMedia, handleSendAudio, handleSyncAvatar, toggleReactionPicker, } = useChatWindow({ selectedChat, activeInstanceId: activeInstance?.instance ?? null, onLoadMoreHistory: handleLoadMoreHistory, }) // ── Mensagem de entrada (hook dedicado) ─────────────────────────────────── const { newMessage, setNewMessage, mentions, setMentions, handleSendMessage } = useMessageInput({ selectedChat, replyingTo, onClearReply: () => setReplyingTo(null), onSendText: handleSendText, }) // ── Painel de Protocolo ─────────────────────────────────────────────────── const [isProtocolOpen, setIsProtocolOpen] = useState(false) const protocolPanel = useProtocolPanel(selectedChat ? String(selectedChat.id) : null) // ── Estado local de UI ──────────────────────────────────────────────────── const [activeTab, setActiveTab] = useState('chats') const [isInstanceDropdownOpen, setIsInstanceDropdownOpen] = useState(false) const [isOptionsMenuOpen, setIsOptionsMenuOpen] = useState(false) const [drafts] = useState>({}) const [isMobile, setIsMobile] = useState(false) const [isNewConvoModalOpen, setIsNewConvoModalOpen] = useState(false) // Permissão de apagar conversa/mensagem (dono ou liberado). Fail-open enquanto carrega. const [canDeleteMsg, setCanDeleteMsg] = useState(true) useEffect(() => { let alive = true HybridBackend.getWaAccess().then(a => { if (alive) setCanDeleteMsg(a.isOwner || a.delete_msg) }).catch(() => {}) return () => { alive = false } }, []) // Preloader: aparece APENAS em hard reload (F5 / URL direta) — nunca em // navegação client-side (router.push). Detectado por: // 1. performance.getEntriesByType('navigation')[0].type === 'reload' // 2. ausência do flag sessionStorage 'nw:inbox-loaded' (primeira visita na sessão) // sessionStorage sobrevive a navegações client-side mas é zerado em nova aba. const [pageReady, setPageReady] = useState(() => { if (typeof window === 'undefined') return false const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined const isHardReload = navEntry ? navEntry.type === 'reload' : (performance.navigation?.type === 1) if (isHardReload) return false return sessionStorage.getItem('nw:inbox-loaded') === '1' }) const prevLoadingRef = useRef(false) const readyTimerRef = useRef | null>(null) useEffect(() => { if (prevLoadingRef.current && !loading) { readyTimerRef.current = setTimeout(() => { setPageReady(true) sessionStorage.setItem('nw:inbox-loaded', '1') }, 400) } prevLoadingRef.current = loading return () => { if (readyTimerRef.current) clearTimeout(readyTimerRef.current) } }, [loading]) const optionsRef = useRef(null) const initialized = useRef(false) // ── Setup mobile + click-outside ───────────────────────────────────────── useEffect(() => { if (initialized.current) return initialized.current = true const resize = () => setIsMobile(window.innerWidth < 768) window.addEventListener('resize', resize) resize() const click = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) setIsInstanceDropdownOpen(false) if (optionsRef.current && !optionsRef.current.contains(e.target as Node)) setIsOptionsMenuOpen(false) } document.addEventListener('mousedown', click) return () => { window.removeEventListener('resize', resize); document.removeEventListener('mousedown', click) } }, []) // ── Handlers ───────────────────────────────────────────────────────────── const handleSelectChat = useCallback((chat: Chat | null) => { handleSelectChatBridge(chat) onChatWindowSelect() setIsProtocolOpen(false) }, [handleSelectChatBridge, onChatWindowSelect]) const handleChatMenuAction = useCallback((action: string, chat: Chat) => { handleMenuAction(action, chat, toggleFavorite) }, [handleMenuAction, toggleFavorite]) const handleFetchChats = useCallback(async () => { await refreshInstances() setIsOptionsMenuOpen(false) }, [refreshInstances]) // Voltar: se há chat aberto, fecha-o; senão, página anterior const handleBack = useCallback(() => { if (selectedChat) { handleSelectChat(null) } else { onNavigate?.('dashboard') } }, [selectedChat, handleSelectChat, onNavigate]) // ESC fecha chat ou vai para página anterior useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') handleBack() } document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) }, [handleBack]) // ── Render ──────────────────────────────────────────────────────────────── return (
{/* Preloader global – visível até o primeiro carregamento de chats completar */} {!pageReady && (
Carregando conversas…
)}
{activeTab === 'chats' && (
{/* Header da sidebar */}
setIsInstanceDropdownOpen(!isInstanceDropdownOpen)} className="flex items-center gap-3 cursor-pointer p-1">
{activeInstance?.instance ? { (e.currentTarget as HTMLImageElement).style.display = 'none' }} /> : }
{activeInstance?.nome || 'Instância'}
{isInstanceDropdownOpen && ( {instances.map(i => ( ))} )}
{/* end left flex */}
{isOptionsMenuOpen && ( )}
{/* Busca */}
setSearchTerm(e.target.value)} />
{/* Lista de chats */}
)} {/* Área principal */}
{/* Overlay branqueador sobre o padrão */}
{activeTab === 'settings' ? ( ) : selectedChat ? ( <>
{ setIsProfileOpen(!isProfileOpen); setIsProtocolOpen(false) }} onBack={() => handleSelectChat(null)} onSyncAvatar={handleSyncAvatar} isChatMenuOpen={isChatMenuOpen} isSyncingAvatar={isSyncingAvatar} isProtocolOpen={isProtocolOpen} hasActiveProtocol={protocolPanel.hasActive} onSearchClick={() => {}} onToggleMenu={() => setIsChatMenuOpen(!isChatMenuOpen)} onProtocolClick={() => { setIsProtocolOpen(!isProtocolOpen); setIsProfileOpen(false) }} /> {}} onDelete={() => {}} canDelete={canDeleteMsg} onEdit={() => {}} quickReactions={['👍', '❤️', '😂', '😮', '😢', '🙏']} getGroupSenderLabel={(msg: any) => { if (msg.direction === 'out') return 'Você' if (msg.participant) return msg.participant if (msg.senderJid) { if (msg.senderJid.endsWith('@lid')) return msg.participant || '' const phone = msg.senderJid.split('@')[0].split(':')[0] return formatPhone(phone, msg.senderJid) } return '' }} /> setReplyingTo(null)} replyingTo={replyingTo} selectedChat={selectedChat} isMobile={isMobile} onSendMedia={handleSendMedia} onSendAudio={handleSendAudio} instanceId={activeInstance?.instance ?? null} mentions={mentions} onMentionsChange={setMentions} />
setIsProfileOpen(false)} /> setIsProtocolOpen(false)} {...protocolPanel} /> ) : (

NewWhats

Selecione uma conversa para começar.

)}
setIsNewConvoModalOpen(false)} activeInstance={mappedActiveInstance as any} connectedInstances={connectedInstances as any} sendMessage={async (instanceId: string, phone: string, text: string) => { try { // ext: inicia nova conversa por telefone (resolve/cria o chat). const res = await nw.post('/conversations', { sessionId: instanceId, phone, text }) return { ok: true, messageId: res?.messageId } } catch { return { ok: false } } }} />
) }