/** * useNewInboxBridge — adapta chatStore + instanceStore para o formato * que os componentes de display legados já consomem. * Nenhum componente visual precisa ser alterado. */ import { useMemo, useCallback, useEffect, useRef } from 'react' import { useChatStore } from '../../store/chatStore' import { useInstanceStore } from '../../store/instanceStore' import { chatApi, messageApi, reconcileApi } from '../../services/chatApiService' import { socketService } from '../../services/socketService' import { formatPhone } from '../../utils/inboxUtils' import type { NewWhatsChat } from '../../types/inboxTypes' // ─── Mapeamentos legado ─────────────────────────────────────────────────────── function chatToLegacy(c: ReturnType['chats'][0]): NewWhatsChat { const isGroup = c.jid.endsWith('@g.us') const jidUser = c.jid.split('@')[0] // Telefone formatado — sempre passa por formatPhone para normalizar E.164 → exibição // c.contactPhone é o número cru do DB (ex: "5511999887766"); formatPhone aplica máscara const formattedPhone = isGroup ? jidUser : formatPhone(c.contactPhone ?? jidUser, c.jid) // 1. Nome de Exibição // Regra estrita: grupos usam SEMPRE o subject (c.name) — nunca o // pushName/contactName do último participante que enviou mensagem. let displayName = isGroup ? (c.name || 'Grupo') : (c.contactName || c.name || formattedPhone || jidUser) // Limpeza final: se por algum motivo ainda vier o JID completo, removemos o sufixo if (displayName.includes('@')) { displayName = displayName.split('@')[0] } // 2. Avatar — o motor (ext /inbox) já devolve a URL pública do CDN do // WhatsApp (pps.whatsapp.net) em contactAvatarUrl; usamos direto (o // carrega cross-origin sem CORS). Quando é null, Avatar.tsx mostra iniciais. const avatar_url: string | null = c.contactAvatarUrl ?? null const avatar_version: string | null = c.contactAvatarVersion ?? null return { id: c.id as any, instance_name: c.instanceId, remote_jid: c.jid, phone: formattedPhone, lead_name: c.contactName, avatar_url, avatar_version, bio: isGroup ? 'Detalhes do Grupo' : (formattedPhone || 'Clique para informações'), displayName, subject: c.protocolNumber ? `#${c.protocolNumber}` : (isGroup ? null : formattedPhone), last_message_text: c.lastMessageBody || null, last_message_time: c.lastMessageAt || c.createdAt, last_message_from_me: c.lastMessageFromMe ? 1 : 0, last_message_status: c.lastMessageStatus?.toLowerCase() ?? null, last_message_type: c.lastMessageType?.toLowerCase() ?? null, last_message_sender: c.lastMessageSender ?? null, unread_count: c.unreadCount, is_pinned: c.isPinned, is_archived: c.isArchived, bot_paused: c.botPaused ?? null, labels: [], } } function messagesToLegacy(msgs: ReturnType['messages'][string]) { return (msgs ?? []).map((m) => ({ id: m.id as any, chat_id: m.chatId as any, message_id: m.messageId, text: m.body ?? '', direction: (m.fromMe ? 'out' : 'in') as 'in' | 'out', timestamp: new Date(m.timestamp).getTime(), status: m.status.toLowerCase(), type: m.type.toLowerCase(), media_url: m.mediaUrl, transcription: (m as any).transcription ?? null, mime_type: m.mimeType, reply_to_id: m.replyToId, // Quoted message — mapeia replyTo do Prisma para o formato do MessageBubble quoted_id: m.replyTo?.id ?? null, quoted_message_id: m.replyTo?.messageId ?? null, quoted_message_text: m.replyTo?.body ?? null, quoted_participant: m.replyTo ? (m.replyTo.fromMe ? 'me' : null) : null, participant: m.pushName ?? null, senderJid: m.senderJid ?? null, sender_jid: m.senderJid ?? null, remote_jid: m.chatId, from_me: m.fromMe ? 1 : 0, rich_payload: (m as any).richPayload ?? null, })) } // ─── Hook principal ─────────────────────────────────────────────────────────── export function useNewInboxBridge() { const initialized = useRef(false) const chatStore = useChatStore() const instanceStore = useInstanceStore() // ── Boot: socket + instâncias ───────────────────────────────────────────── useEffect(() => { if (initialized.current) return initialized.current = true // Satélite: o socketService lê o token do scoreodonto internamente. socketService.connect() chatStore.initSocketListeners() instanceStore.initSocketListeners() instanceStore.loadInstances() }, []) // ── Carregar chats quando instância ativa muda ───────────────────────────── useEffect(() => { const { activeInstanceId } = instanceStore if (!activeInstanceId) return chatStore.loadChats(activeInstanceId) socketService.joinInstance(activeInstanceId) // Reconcilia nomes em background (cache 30min — leve em escala) reconcileApi.reconcileNames(activeInstanceId).catch(() => {}) }, [instanceStore.activeInstanceId]) // ── Adaptação: instâncias → legado ──────────────────────────────────────── const instancesLegacy = useMemo( () => instanceStore.instances.map((i) => ({ instance: i.id, nome: i.name, status: i.status === 'CONNECTED' ? 'connected' : i.status.toLowerCase(), phone: i.phone, avatar: null, profilePictureUrl: null, })), [instanceStore.instances] ) const activeInstanceLegacy = useMemo( () => instancesLegacy.find((i) => i.instance === instanceStore.activeInstanceId) ?? null, [instancesLegacy, instanceStore.activeInstanceId] ) // ── Adaptação: chats → legado ───────────────────────────────────────────── const chatsLegacy = useMemo(() => chatStore.chats.map(chatToLegacy), [chatStore.chats]) const selectedChatLegacy = useMemo(() => { if (!chatStore.activeChatId) return null const c = chatStore.chats.find((c) => c.id === chatStore.activeChatId) return c ? chatToLegacy(c) : null }, [chatStore.activeChatId, chatStore.chats]) const messagesLegacy = useMemo(() => { if (!chatStore.activeChatId) return [] const raw = chatStore.messages[chatStore.activeChatId] ?? [] const mapped = messagesToLegacy(raw) console.log('[bridge] messagesLegacy recomputed:', { activeChatId: chatStore.activeChatId, rawCount: raw.length, mappedCount: mapped.length }) return mapped }, [chatStore.activeChatId, chatStore.messages]) // ── Handlers ────────────────────────────────────────────────────────────── const handleSelectChat = useCallback(async (chat: NewWhatsChat | null) => { if (!chat) { useChatStore.getState().setActiveChat(null); return } await chatStore.selectChat(String(chat.id)) }, [chatStore]) const handleSendText = useCallback(async (text: string, mentions?: string[], replyToMessageId?: string) => { if (!chatStore.activeChatId || !instanceStore.activeInstanceId) return const chat = chatStore.chats.find((c) => c.id === chatStore.activeChatId) if (!chat) return await chatStore.sendMessage(instanceStore.activeInstanceId, chat.jid, text, replyToMessageId, mentions) }, [chatStore, instanceStore.activeInstanceId]) const handleLoadMoreHistory = useCallback(async () => { if (!chatStore.activeChatId) return { hasMore: false } const oldest = chatStore.messages[chatStore.activeChatId]?.[0] if (!oldest) return { hasMore: false } return chatStore.loadMessages(chatStore.activeChatId, { before: oldest.timestamp }) }, [chatStore]) const handleSetActiveInstance = useCallback((legacyInstance: any) => { instanceStore.setActiveInstance(legacyInstance?.instance ?? null) }, [instanceStore]) const handleRefreshInstances = useCallback(async () => { await instanceStore.loadInstances() }, [instanceStore]) const handleConnectInstance = useCallback(async (instanceId: string) => { await instanceStore.connectInstance(instanceId) }, [instanceStore]) const handleArchiveChat = useCallback(async (chatId: string, archived: boolean) => { await chatApi.archive(chatId, archived) useChatStore.getState().patchChat(chatId, { isArchived: archived }) }, []) const handlePinChat = useCallback(async (chatId: string, pinned: boolean) => { await chatApi.pin(chatId, pinned) useChatStore.getState().patchChat(chatId, { isPinned: pinned }) }, []) const handleDeleteChat = useCallback(async (chatId: string) => { await chatApi.delete(chatId) useChatStore.getState().setChats( useChatStore.getState().chats.filter((c) => c.id !== chatId) ) if (useChatStore.getState().activeChatId === chatId) { useChatStore.getState().setActiveChat(null) } }, []) const handleMarkRead = useCallback(async (chatId: string) => { await chatApi.markRead(chatId) useChatStore.getState().patchChat(chatId, { unreadCount: 0 }) }, []) const handleSearchMessages = useCallback(async (q: string) => { if (!instanceStore.activeInstanceId || q.length < 2) return [] return messageApi.search(instanceStore.activeInstanceId, q) }, [instanceStore.activeInstanceId]) // action é a string-key emitida por ChatListItem (ex: 'archive', 'pin', ...) // onFavoriteToggle é injetado pelo componente pai (inbox page) pois favoritos são estado local const handleMenuAction = useCallback(( action: string, chat: NewWhatsChat, onFavoriteToggle?: (jid: string) => void ) => { const chatId = String(chat.id) switch (action) { case 'archive': handleArchiveChat(chatId, !chat.is_archived); break case 'pin': handlePinChat(chatId, !chat.is_pinned); break case 'mark_read': handleMarkRead(chatId); break case 'delete': handleDeleteChat(chatId); break case 'favorite': onFavoriteToggle?.(chat.remote_jid); break } }, [handleArchiveChat, handlePinChat, handleMarkRead, handleDeleteChat]) return { instances: instancesLegacy, activeInstance: activeInstanceLegacy, chats: chatsLegacy, selectedChat: selectedChatLegacy, messages: messagesLegacy, loadingChats: chatStore.chatsLoading, loadingMessages: chatStore.messagesLoading, activeInstanceId: instanceStore.activeInstanceId, qrByInstance: instanceStore.qrByInstance, setActiveInstance: handleSetActiveInstance, refreshInstances: handleRefreshInstances, handleSelectChat, handleSendText, handleLoadMoreHistory, handleConnectInstance, handleArchiveChat, handlePinChat, handleDeleteChat, handleMarkRead, handleSearchMessages, handleMenuAction, presenceByJid: {} as Record, isFavoriteChat: (_: NewWhatsChat) => false, } }