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 [deliveryAccount, setDeliveryAccount] = useState(account) const [appSecret, setAppSecret] = useState('') const [deliveryBusy, setDeliveryBusy] = useState(false) const line = deliveryAccount.primary_phone?.display_phone || deliveryAccount.name const canSend = deliveryAccount.messaging_status === 'active' && deliveryAccount.usable const repliesLive = deliveryAccount.webhooks_status === 'active' async function saveDelivery(event: FormEvent) { event.preventDefault() setDeliveryBusy(true) setError('') try { setDeliveryAccount(await whatsappApi.enableDelivery(account.id, appSecret.trim())) setAppSecret('') } catch (reason) { setError(reason instanceof Error ? reason.message : 'Replies could not be turned on') } finally { setDeliveryBusy(false) } } async function loadMessages() { setMessages(await whatsappApi.messages(account.id)) } useEffect(() => { Promise.all([ whatsappApi.messages(account.id), account.status === 'disconnected' ? Promise.resolve({ available: false, templates: [] as WhatsAppTemplateRow[] }) : 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 &&

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

} {canSend && !repliesLive && (
void saveDelivery(event)}>

WhatsApp accepted messages from this number, but delivery updates and replies are still going to the Meta app that created the token. Paste that app’s App secret so they arrive here. In Meta: the app → App settings → Basic → App secret.

)}
{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" />
)}