Files
instrucoes_gerais/clube67/newwhats.local/frontend/pages/inbox/index.tsx
T

416 lines
23 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Search,
MoreVertical,
User,
MessageSquare,
ChevronDown,
RefreshCw,
MessageSquarePlus,
RotateCw,
ArrowLeft,
} from 'lucide-react';
import { useRouter } from 'next/router';
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 { messageApi } from '../../services/chatApiService';
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 InputBar from '../../components/InputBar';
import NewConversationModal from '../../components/NewConversationModal';
import ContactProfile from '../../components/ContactProfile';
import ProtocolPanel from '../../components/ProtocolPanel';
type Chat = NewWhatsChat;
export default function WhatsAppInboxPage() {
const router = useRouter()
// ── 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<TabType>('chats')
const [isInstanceDropdownOpen, setIsInstanceDropdownOpen] = useState(false)
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = useState(false)
const [drafts] = useState<Record<string, string>>({})
const [isMobile, setIsMobile] = useState(false)
const [isNewConvoModalOpen, setIsNewConvoModalOpen] = useState(false)
const optionsRef = useRef<HTMLDivElement>(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 {
router.back()
}
}, [selectedChat, handleSelectChat, router])
// 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 (
<div className="flex h-screen w-full bg-[#f0f2f5] overflow-hidden select-none font-sans text-[#111b21]">
<div className="flex flex-1 overflow-hidden h-full">
<ThinSidebar activeTab={activeTab} setActiveTab={setActiveTab} activeInstance={activeInstance} />
{activeTab === 'chats' && (
<div className={`${isMobile ? (selectedChat ? 'hidden' : 'w-full') : 'w-[400px]'} flex flex-col bg-white border-r border-[#d1d7db] shrink-0 z-20`}>
{/* Header da sidebar */}
<div className="h-[60px] px-4 flex items-center justify-between bg-[#f0f2f5] border-b border-[#d1d7db] shrink-0">
<div className="flex items-center gap-1">
<button
onClick={handleBack}
title={selectedChat ? 'Voltar aos chats' : 'Voltar'}
className="p-2 hover:bg-black/5 rounded-full text-[#54656f] shrink-0"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="relative" ref={dropdownRef}>
<div onClick={() => setIsInstanceDropdownOpen(!isInstanceDropdownOpen)} className="flex items-center gap-3 cursor-pointer p-1">
<div className="w-10 h-10 rounded-full bg-white border border-black/5 overflow-hidden flex items-center justify-center">
{activeInstance?.instance
? <img
src={getAvatarProxyUrl({
instance_name: activeInstance.instance,
remote_jid: activeInstance.phone ? `${activeInstance.phone}@s.whatsapp.net` : null,
}) || activeInstance.avatar || ''}
className="w-full h-full object-cover" alt=""
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
/>
: <User className="text-gray-400" />}
</div>
<span className="font-bold text-sm truncate max-w-[120px]">{activeInstance?.nome || 'Instância'}</span>
<ChevronDown className={`w-4 h-4 transition-all ${isInstanceDropdownOpen ? 'rotate-180' : ''}`} />
</div>
<AnimatePresence>
{isInstanceDropdownOpen && (
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}
className="absolute left-0 top-full bg-white shadow-2xl rounded-xl border border-gray-100 w-72 z-50 py-2">
{instances.map(i => (
<button key={i.instance} onClick={() => { selectInstance(i); setIsInstanceDropdownOpen(false) }}
className="w-full p-3 hover:bg-gray-50 flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gray-100 overflow-hidden">
<img
src={getAvatarProxyUrl({
instance_name: i.instance,
remote_jid: i.phone ? `${i.phone}@s.whatsapp.net` : null,
}) || i.avatar || ''}
alt="" className="w-full h-full object-cover"
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
/>
</div>
<div className="text-left">
<span className="text-sm font-medium block">{i.nome}</span>
<span className={`text-xs ${i.status === 'connected' ? 'text-green-500' : 'text-gray-400'}`}>{i.status}</span>
</div>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>{/* end left flex */}
<div className="flex items-center gap-1">
<div className="relative" ref={optionsRef}>
<button onClick={() => setIsOptionsMenuOpen(!isOptionsMenuOpen)} className="p-2 hover:bg-black/5 rounded-full">
<MoreVertical className="w-5 h-5 text-[#54656f]" />
</button>
<AnimatePresence>
{isOptionsMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute right-0 top-full mt-1 bg-white shadow-2xl rounded-xl border border-gray-100 w-56 z-50 py-2"
>
<button
onClick={handleFetchChats}
disabled={loading}
className="w-full flex items-center gap-3 px-4 py-2.5 text-[14px] text-[#111b21] hover:bg-[#f5f6f6] transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Atualizar chats
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
{/* Busca */}
<div className="px-4 py-2 flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 w-4 h-4 text-gray-400" />
<input type="text" placeholder="Pesquisar..."
className="w-full bg-[#f0f2f5] h-[35px] pl-10 rounded-lg text-sm border-none focus:ring-0"
value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
</div>
<button onClick={() => setIsNewConvoModalOpen(true)}
className="w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-black/5">
<MessageSquarePlus className="w-4 h-4" />
</button>
</div>
{/* Lista de chats */}
<ChatSidebar
chats={chatsByScope}
loading={loading}
selectedChat={selectedChat}
searchTerm={searchTerm}
onSelectChat={handleSelectChat}
onMenuAction={handleChatMenuAction}
activeFilter={activeFilter}
selectedLabelFilter={null}
chatScope={chatScope}
activeCustomListName={null}
presenceByJid={presenceByJid}
isFavoriteChat={isFavoriteChat}
drafts={drafts}
/>
</div>
)}
{/* Área principal */}
<div
className={`${isMobile ? (selectedChat ? 'w-full' : 'hidden') : 'flex-1'} flex h-full relative overflow-hidden`}
style={{ backgroundImage: "url('/chat-bg.png')", backgroundRepeat: 'repeat', backgroundColor: '#b5bdb5' }}
>
{/* Overlay branqueador sobre o padrão */}
<div className="absolute inset-0 bg-white/75 pointer-events-none z-0" />
{selectedChat ? (
<>
<div className="flex-1 flex flex-col h-full relative z-[1] overflow-hidden border-r border-[#d1d7db]">
<InboxHeader
selectedChat={selectedChat}
isMobile={isMobile}
isTyping={isTyping}
onAvatarClick={() => { 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) }}
/>
<MessageArea
messages={messages as any}
selectedChat={selectedChat}
scrollRef={scrollRef}
msgLoading={loadingMessages}
onScroll={handleScroll}
historyLoadingMore={loadingMore}
hasMoreHistory={hasMoreHistory}
reactionPickerFor={reactionPickerFor}
onReply={setReplyingTo}
onToggleReactionPicker={toggleReactionPicker}
onReact={handleReactToMessage}
onForward={() => {}}
onDelete={() => {}}
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 ''
}}
/>
<InputBar
newMessage={newMessage}
onNewMessageChange={setNewMessage}
onSend={handleSendMessage}
onPresence={handleTyping}
onCancelReply={() => setReplyingTo(null)}
replyingTo={replyingTo}
selectedChat={selectedChat}
isMobile={isMobile}
onSendMedia={handleSendMedia}
onSendAudio={handleSendAudio}
instanceId={activeInstance?.instance ?? null}
mentions={mentions}
onMentionsChange={setMentions}
/>
</div>
<ContactProfile
isOpen={isProfileOpen && !isProtocolOpen && !isMobile}
chat={selectedChat}
messages={messages as any}
onClose={() => setIsProfileOpen(false)}
/>
<ProtocolPanel
isOpen={isProtocolOpen && !isMobile}
chatId={selectedChat ? String(selectedChat.id) : null}
onClose={() => setIsProtocolOpen(false)}
{...protocolPanel}
/>
</>
) : (
<div className="flex-1 flex flex-col items-center justify-center p-10 bg-[#f0f2f5]">
<MessageSquare className="w-20 h-20 opacity-10 mb-8" />
<h1 className="text-2xl font-light text-[#41525d] mb-4">NewWhats</h1>
<p className="text-sm text-[#667781]">Selecione uma conversa para começar.</p>
</div>
)}
</div>
</div>
<NewConversationModal
isOpen={isNewConvoModalOpen}
onClose={() => setIsNewConvoModalOpen(false)}
activeInstance={mappedActiveInstance as any}
connectedInstances={connectedInstances as any}
sendMessage={async (instanceId: string, phone: string, text: string) => {
try {
const res = await messageApi.send(instanceId, `${phone}@s.whatsapp.net`, text)
return { ok: true, messageId: res?.messageId }
} catch {
return { ok: false }
}
}}
/>
</div>
)
}