21bebd3256
- Drawer de WhatsApp focado (WhatsChatDrawer) aberto do slot do agendamento, com dropdown de números do paciente + grupo familiar e fluxo de iniciar conversa - Auto-provisionamento de conta no motor: eager no cadastro/convite + self-heal no proxy (404 "conta não encontrada" → provisiona e refaz o request) - Ownership por sessão (wa_session_authz): dono do workspace (owner_id ou vínculo donoclinica/donoconsultorio) tem poder total; delega re-scan, excluir sessão, apagar conversa/mensagem, acesso ao Inbox e à Secretária — por conta - Enforcement no proxy: guardSessionAction (scan/rescan/delete sessão), guardAreaAccess (inbox/secretaria), guardInboxDestructive (apagar conversa/msg) - Endpoints /api/nw/authz (dono gerencia) e /api/nw/access (acesso agregado) - Assinatura do operador nas mensagens (*Nome*, desambiguação de homônimos → *Rui C.*) - UI: painel "Gerenciar acesso" com 5 toggles; menu e botões de apagar condicionados - Proxy de mídia /api/nw/media + re-download sob demanda Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
485 lines
27 KiB
TypeScript
485 lines
27 KiB
TypeScript
|
||
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<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)
|
||
|
||
// 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<boolean>(() => {
|
||
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<ReturnType<typeof setTimeout> | 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<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 {
|
||
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 (
|
||
<div className="flex h-full w-full bg-[#f0f2f5] overflow-hidden select-none font-sans text-[#111b21]">
|
||
{/* Preloader global – visível até o primeiro carregamento de chats completar */}
|
||
<AnimatePresence>
|
||
{!pageReady && (
|
||
<motion.div
|
||
key="inbox-preloader"
|
||
exit={{ opacity: 0 }}
|
||
transition={{ duration: 0.4 }}
|
||
className="fixed inset-0 z-[9999] flex items-center justify-center bg-[#070b14]"
|
||
>
|
||
<div className="flex flex-col items-center gap-5">
|
||
<motion.div
|
||
animate={{ opacity: [1, 0.2, 1] }}
|
||
transition={{ duration: 1.6, repeat: Infinity, ease: 'easeInOut' }}
|
||
>
|
||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" className="w-20 h-20">
|
||
<defs>
|
||
<linearGradient id="wpp-grad" x1="50" y1="100" x2="50" y2="0" gradientUnits="userSpaceOnUse">
|
||
<stop offset="0" stopColor="#20b038"/>
|
||
<stop offset="1" stopColor="#60d66a"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<circle cx="50" cy="50" r="50" fill="url(#wpp-grad)"/>
|
||
<path d="M50 15.5c-19.05 0-34.5 15.45-34.5 34.5 0 6.36 1.72 12.3 4.73 17.4L15.5 84.5l17.6-4.62a34.36 34.36 0 0 0 16.9 4.42c19.05 0 34.5-15.45 34.5-34.5S69.05 15.5 50 15.5z" fill="rgba(0,0,0,0.12)"/>
|
||
<path d="M68.5 60.3c-1.1-.57-6.53-3.22-7.54-3.59-.99-.37-1.72-.55-2.44.55-.73 1.1-2.82 3.59-3.46 4.32-.64.73-1.28.82-2.38.27-6.53-3.26-10.81-5.83-15.1-13.22-1.14-1.97.24-1.83.65-3.05.34-1 .17-1.84-.1-2.39-.27-.55-2.44-5.99-3.4-8.2-.9-2.16-1.81-1.86-2.49-1.89-.64-.03-1.38-.04-2.12-.04s-1.93.28-2.94 1.38c-1.01 1.1-3.87 3.78-3.87 9.22s3.96 10.69 4.51 11.43c.55.73 7.8 11.9 18.9 16.7 7.02 3.03 9.76 3.28 13.28 2.76 2.13-.32 6.53-2.67 7.45-5.25.92-2.58.92-4.79.64-5.25-.27-.46-1-.73-2.09-1.27z" fill="#fff"/>
|
||
</svg>
|
||
</motion.div>
|
||
<span className="text-slate-500 text-sm">Carregando conversas…</span>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
<div className="flex flex-1 overflow-hidden h-full">
|
||
<ThinSidebar activeTab={activeTab} setActiveTab={setActiveTab} activeInstance={activeInstance} onNavigate={onNavigate} />
|
||
|
||
{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 || undefined}
|
||
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 || undefined}
|
||
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}
|
||
canDelete={canDeleteMsg}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Área principal */}
|
||
<div
|
||
className={`${isMobile ? ((selectedChat || activeTab === 'settings') ? '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" />
|
||
{activeTab === 'settings' ? (
|
||
<SettingsPanel instanceId={activeInstance?.instance ?? null} />
|
||
) : 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={() => {}}
|
||
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 ''
|
||
}}
|
||
/>
|
||
<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 {
|
||
// 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 }
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|