This commit is contained in:
debu-2 2026-10-15 03:39:09 +05:30
parent 9f03e5df32
commit 33b6b5d26f
4 changed files with 159 additions and 50 deletions

View file

@ -77,6 +77,36 @@ function initials(name: string) {
return name.replace(/\D/g, '').slice(-2) || 'WA'
}
function buildThreads(items: CustomerWhatsAppMessage[], readPeers: Set<string>, starred: Set<string>) {
const grouped = new Map<string, CustomerWhatsAppMessage[]>()
for (const message of items) {
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))
}
export function CustomerWhatsAppInbox({ account, onBack }: { account: WhatsAppAccount; onBack: () => void }) {
const [messages, setMessages] = useState<CustomerWhatsAppMessage[]>([])
const [templates, setTemplates] = useState<WhatsAppTemplateRow[]>([])
@ -101,6 +131,7 @@ export function CustomerWhatsAppInbox({ account, onBack }: { account: WhatsAppAc
const [buttonTwo, setButtonTwo] = useState('')
const [buttonThree, setButtonThree] = useState('')
const [threadQuery, setThreadQuery] = useState('')
const [searchHits, setSearchHits] = useState<CustomerWhatsAppMessage[] | null>(null)
const [listFilter, setListFilter] = useState<ListFilter>('all')
const [readPeers, setReadPeers] = useState<Set<string>>(new Set())
const [starred, setStarred] = useState<Set<string>>(new Set())
@ -132,66 +163,74 @@ export function CustomerWhatsAppInbox({ account, onBack }: { account: WhatsAppAc
}
async function loadMessages() {
setMessages(await whatsappApi.messages(account.id))
setMessages(await whatsappApi.messages(account.id, { limit: 500 }))
}
useEffect(() => {
let cancelled = false
Promise.all([
whatsappApi.messages(account.id),
whatsappApi.messages(account.id, { limit: 500 }),
account.status === 'disconnected'
? Promise.resolve({ available: false, templates: [] as WhatsAppTemplateRow[] })
: whatsappApi.templates(account.id).catch(() => ({ available: false, templates: [] as WhatsAppTemplateRow[] })),
])
.then(([items, templatePayload]) => {
if (cancelled) return
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<string, CustomerWhatsAppMessage[]>()
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),
}
.catch((reason) => {
if (!cancelled) setError(reason instanceof Error ? reason.message : 'Unable to load conversations')
})
.sort((a, b) => b.latest.created_at.localeCompare(a.latest.created_at))
}, [messages, readPeers, starred])
.finally(() => {
if (!cancelled) setLoading(false)
})
const timer = window.setInterval(() => {
whatsappApi.messages(account.id, { limit: 500 }).then((items) => {
if (!cancelled) setMessages(items)
}).catch(() => undefined)
}, 8000)
return () => {
cancelled = true
window.clearInterval(timer)
}
}, [account.id, account.status])
useEffect(() => {
const query = threadQuery.trim()
if (!query) {
setSearchHits(null)
return
}
let cancelled = false
const timer = window.setTimeout(() => {
whatsappApi.messages(account.id, { q: query })
.then((items) => {
if (!cancelled) setSearchHits(items)
})
.catch(() => {
if (!cancelled) setSearchHits([])
})
}, 300)
return () => {
cancelled = true
window.clearTimeout(timer)
}
}, [account.id, threadQuery])
const inboxThreads = useMemo(() => buildThreads(messages, readPeers, starred), [messages, readPeers, starred])
const searchThreads = useMemo(
() => (threadQuery.trim() && searchHits ? buildThreads(searchHits, readPeers, starred) : null),
[searchHits, readPeers, starred, threadQuery],
)
const threads = searchThreads ?? inboxThreads
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 active = threads.find((thread) => thread.peer === selectedPeer) || inboxThreads.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
@ -322,7 +361,7 @@ export function CustomerWhatsAppInbox({ account, onBack }: { account: WhatsAppAc
<button type="button" className={listFilter === 'important' ? 'active' : ''} onClick={() => setListFilter('important')}>Important <em>{importantTotal}</em></button>
</div>
<div className="wa-thread-list">
{visibleThreads.length === 0 && <p className="wa-empty">No conversations yet.</p>}
{visibleThreads.length === 0 && <p className="wa-empty">{threadQuery.trim() ? 'No conversations match this search.' : 'No conversations yet.'}</p>}
{visibleThreads.map((thread) => (
<button key={thread.peer} type="button" className={`wa-thread ${selectedPeer === thread.peer ? 'active' : ''}`} onClick={() => openThread(thread.peer)}>
<span className="wa-avatar wa-avatar-green">{thread.initials}</span>

View file

@ -1295,8 +1295,12 @@ export const whatsappApi = {
method: 'POST',
body: JSON.stringify({ app_secret: appSecret }),
}),
messages: (accountId: string) =>
jsonRequest<CustomerWhatsAppMessage[]>(`/api/v1/whatsapp/accounts/${accountId}/messages`),
messages: (accountId: string, options?: { limit?: number; q?: string }) => {
const params = new URLSearchParams()
params.set('limit', String(options?.limit ?? 500))
if (options?.q) params.set('q', options.q)
return jsonRequest<CustomerWhatsAppMessage[]>(`/api/v1/whatsapp/accounts/${accountId}/messages?${params}`)
},
sendAccountMessage: (accountId: string, payload: CustomerWhatsAppSend) =>
jsonRequest<CustomerWhatsAppMessage>(`/api/v1/whatsapp/accounts/${accountId}/messages/send`, {
method: 'POST',