import { Check, CheckCheck, ChevronLeft, Image as ImageIcon, MessageCircle, MessageSquareText, MoreHorizontal, Paperclip, Plus, RefreshCw, Search, Send, Smile, Star, UserRound, X, } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import type { FormEvent } from 'react' import { API_BASE_URL } from '../../lib/apiBase' import { whatsappApi, type CustomerWhatsAppMessage, type CustomerWhatsAppSend, type WhatsAppAccount, type WhatsAppTemplateRow, } from '../../lib/api' type SendMode = 'text' | 'template' | 'media' | 'buttons' type ListFilter = 'all' | 'unread' | 'important' type ProfileTab = 'details' | 'events' const REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '🙏'] function formatWaNumber(value: string) { const digits = value.replace(/\D/g, '') if (digits.length === 12 && digits.startsWith('91')) { const local = digits.slice(2) return `+91 ${local.slice(0, 5)} ${local.slice(5)}` } if (digits.length === 10) return `+91 ${digits.slice(0, 5)} ${digits.slice(5)}` return value.startsWith('+') ? value : `+${digits || value}` } function formatClock(value: string) { return new Date(value).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) } function formatChatTime(value: string) { const date = new Date(value) const now = new Date() if (date.toDateString() === now.toDateString()) return formatClock(value) const yesterday = new Date(now) yesterday.setDate(now.getDate() - 1) if (date.toDateString() === yesterday.toDateString()) return 'Yesterday' return date.toLocaleDateString([], { month: 'short', day: 'numeric' }) } function formatDayLabel(value: string) { const date = new Date(value) const now = new Date() if (date.toDateString() === now.toDateString()) return 'Today' const yesterday = new Date(now) yesterday.setDate(now.getDate() - 1) if (date.toDateString() === yesterday.toDateString()) return 'Yesterday' return date.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }) } function formatWhen(value: string) { return new Date(value).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) } function initials(name: string) { const parts = name.split(/\s+/).filter(Boolean) if (parts.length >= 2) return `${parts[0][0]}${parts[1][0]}`.toUpperCase() return name.replace(/\D/g, '').slice(-2) || 'WA' } export function CustomerWhatsAppInbox({ account, onBack }: { account: WhatsAppAccount; onBack: () => void }) { const [messages, setMessages] = useState([]) const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(true) const [selectedPeer, setSelectedPeer] = useState(null) const [startingNew, setStartingNew] = useState(false) const [newPeer, setNewPeer] = useState('') const [sendMode, setSendMode] = useState('text') const [error, setError] = useState('') const [sending, setSending] = useState(false) const [textBody, setTextBody] = useState('') const [replyTo, setReplyTo] = useState('') const [replyPreview, setReplyPreview] = useState('') const [templateName, setTemplateName] = useState('') const [templateLanguage, setTemplateLanguage] = useState('en') const [mediaType, setMediaType] = useState<'image' | 'document' | 'audio' | 'video'>('image') const [mediaLink, setMediaLink] = useState('') const [mediaCaption, setMediaCaption] = useState('') const [buttonBody, setButtonBody] = useState('') const [buttonOne, setButtonOne] = useState('') const [buttonTwo, setButtonTwo] = useState('') const [buttonThree, setButtonThree] = useState('') const [threadQuery, setThreadQuery] = useState('') const [listFilter, setListFilter] = useState('all') const [readPeers, setReadPeers] = useState>(new Set()) const [starred, setStarred] = useState>(new Set()) const [profileTab, setProfileTab] = useState('details') const [moreOpen, setMoreOpen] = useState(false) const [detailsId, setDetailsId] = useState(null) const [reactFor, setReactFor] = useState(null) const [emojiOpen, setEmojiOpen] = useState(false) const chatEndRef = useRef(null) const line = account.primary_phone?.display_phone || account.name const canSend = account.messaging_status === 'active' && account.usable async function loadMessages() { setMessages(await whatsappApi.messages(account.id)) } useEffect(() => { Promise.all([ whatsappApi.messages(account.id), whatsappApi.templates(account.id).catch(() => ({ available: false, templates: [] as WhatsAppTemplateRow[] })), ]) .then(([items, templatePayload]) => { setMessages(items) setTemplates(templatePayload.templates.filter((item) => item.status === 'APPROVED' || !item.status)) }) .catch((reason) => setError(reason instanceof Error ? reason.message : 'Unable to load conversations')) .finally(() => setLoading(false)) const timer = window.setInterval(() => { loadMessages().catch(() => undefined) }, 8000) return () => window.clearInterval(timer) }, [account.id]) const threads = useMemo(() => { const grouped = new Map() for (const message of messages) { if (message.message_type === 'status') continue const peer = message.direction === 'inbound' ? message.from : message.to if (!peer) continue const list = grouped.get(peer) ?? [] list.push(message) grouped.set(peer, list) } return [...grouped.entries()] .map(([peer, thread]) => { const sorted = [...thread].sort((a, b) => a.created_at.localeCompare(b.created_at)) const latest = sorted[sorted.length - 1] const name = sorted.find((item) => item.contact_name)?.contact_name const unread = readPeers.has(peer) ? 0 : sorted.filter((item) => item.direction === 'inbound' && item.status !== 'read').length return { peer, title: name || formatWaNumber(peer), phoneLabel: formatWaNumber(peer), initials: initials(name || peer), messages: sorted, latest, unread, important: starred.has(peer), } }) .sort((a, b) => b.latest.created_at.localeCompare(a.latest.created_at)) }, [messages, readPeers, starred]) const visibleThreads = threads.filter((thread) => { const query = threadQuery.trim().toLowerCase() if (query && !`${thread.title} ${thread.phoneLabel} ${thread.latest.body || ''}`.toLowerCase().includes(query)) return false if (listFilter === 'unread') return thread.unread > 0 if (listFilter === 'important') return thread.important return true }) const active = threads.find((thread) => thread.peer === selectedPeer) || null const activeThread = active?.messages ?? [] const unreadTotal = threads.reduce((sum, thread) => sum + thread.unread, 0) const importantTotal = threads.filter((thread) => thread.important).length useEffect(() => { chatEndRef.current?.scrollIntoView({ block: 'end' }) }, [activeThread.length, selectedPeer]) function openThread(peer: string) { setSelectedPeer(peer) setStartingNew(false) setReadPeers((current) => new Set(current).add(peer)) setMoreOpen(false) } async function sendReaction(message: CustomerWhatsAppMessage, emoji: string) { if (!message.wa_message_id) return setSending(true) setError('') try { await whatsappApi.sendAccountMessage(account.id, { to: message.from, message_type: 'reaction', reaction_message_id: message.wa_message_id, reaction_emoji: emoji, }) setReactFor(null) await loadMessages() } catch (reason) { setError(reason instanceof Error ? reason.message : 'Unable to send reaction') } finally { setSending(false) } } async function sendMessage(event: FormEvent) { event.preventDefault() const peer = active?.peer || newPeer if (!peer) { setError('Enter a WhatsApp number') return } setSending(true) setError('') const payload: CustomerWhatsAppSend = { to: peer, message_type: sendMode === 'buttons' ? 'interactive_buttons' : sendMode } if (replyTo) payload.reply_to = replyTo if (sendMode === 'text') payload.text = textBody.trim() if (sendMode === 'template') { payload.template_name = templateName payload.template_language = templateLanguage } if (sendMode === 'media') { payload.message_type = mediaType payload.media_link = mediaLink.trim() payload.media_caption = mediaCaption.trim() } if (sendMode === 'buttons') { payload.interactive_body = buttonBody.trim() payload.interactive_buttons = [buttonOne, buttonTwo, buttonThree] .map((title) => title.trim()) .filter(Boolean) .map((title, index) => ({ id: `btn_${index + 1}`, title })) } try { const sent = await whatsappApi.sendAccountMessage(account.id, payload) setSelectedPeer(sent.to) setStartingNew(false) setNewPeer('') setTextBody('') setButtonBody('') setMediaCaption('') setReplyTo('') setReplyPreview('') setSendMode('text') await loadMessages() } catch (reason) { setError(reason instanceof Error ? reason.message : 'Unable to send message') } finally { setSending(false) } } function mediaUrl(mediaId: string) { return `${API_BASE_URL}/api/v1/whatsapp/accounts/${account.id}/media/${mediaId}` } if (loading) return

Loading WhatsApp conversations…

return (

{account.business_name || account.name}

Conversations on {formatWaNumber(line)}.

{canSend ? 'Messaging' : 'Messaging unavailable'}
{!canSend &&

{account.last_error || 'You can read messages already delivered here. Sending stays off until Meta connects this number.'}

}
{active ? ( <> {active.initials}
{active.title} WhatsApp
) : (
{startingNew ? 'New conversation' : 'Select a conversation'} {startingNew ? 'Enter a WhatsApp number' : formatWaNumber(line)}
)}
{moreOpen && active && (
)}
{!active && !startingNew &&

Messages for this WhatsApp number appear here.

} {activeThread.map((item, index) => { const previous = activeThread[index - 1] const showDay = !previous || new Date(previous.created_at).toDateString() !== new Date(item.created_at).toDateString() const outbound = item.direction === 'outbound' return (
{showDay &&
{formatDayLabel(item.created_at)}
}
{item.media?.id && ['image', 'sticker'].includes(item.message_type) && } {item.media?.id && item.message_type === 'video' &&
{!outbound && } {!outbound && item.wa_message_id && } {reactFor === item.id && ( {REACTIONS.map((emoji) => )} )}
) })}
void sendMessage(event)}>
{(['text', 'template', 'media', 'buttons'] as const).map((id) => ( ))}
{replyPreview && (
Replying

{replyPreview}

)} {(startingNew || !active) && setNewPeer(event.target.value)} placeholder="Recipient number, e.g. 919453226814" />} {sendMode === 'template' && (
setTemplateLanguage(event.target.value)} aria-label="Language code" />
)} {sendMode === 'media' && (
setMediaLink(event.target.value)} placeholder="Public file link" required />
)} {sendMode === 'buttons' && (
setButtonOne(event.target.value)} placeholder="Button 1" required /> setButtonTwo(event.target.value)} placeholder="Button 2" /> setButtonThree(event.target.value)} placeholder="Button 3" />
)}