template
This commit is contained in:
parent
9f03e5df32
commit
33b6b5d26f
4 changed files with 159 additions and 50 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue