mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Add full support for WhatsApp Cloud API interactive messages across the inbox, automations, and a new quick-replies feature. Extends the interactive plumbing that previously existed only for the Flows engine. Sending - send-message core handles `interactive` (reply buttons ≤3, list ≤10 rows) via the existing Meta senders; validated server-side before send. - Inbox composer gains a "+" menu → builder dialog with live WhatsApp-style preview + Meta char limits (button 20, list row 24, body 1024). - Automation "Send Buttons" / "Send List" steps in the builder. - Public v1 /messages accepts type=interactive. Storage / round-trip - migration 035: messages.interactive_payload JSONB + quick_replies table. - Outbound interactive persists the structured payload (composer, automations, and Flows) so the thread re-renders the buttons/rows. Receiving - Bubble renders outbound interactive via the shared preview; inbound taps (button_reply/list_reply) keep the "chosen option" affordance. Automations - New `interactive_reply` trigger matches on tapped button/list IDs (exact), enabling chained multi-step menus. Quick replies - New account-scoped quick_replies (text or interactive snippets), managed in Settings and insertable from the composer; "Save as quick reply" in the builder. Shared core in src/lib/whatsapp/interactive.ts + src/components/interactive/. Includes unit tests for the validator, send-param validation, the interactive_reply trigger match, and the new automation step/trigger validation. typecheck + lint clean; full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
import type { AutomationTriggerType } from '@/types'
|
|
|
|
export interface TriggerMeta {
|
|
label: string
|
|
/** Tailwind classes for the Badge pill on the list row. */
|
|
pillClass: string
|
|
}
|
|
|
|
export const TRIGGER_META: Record<AutomationTriggerType, TriggerMeta> = {
|
|
new_message_received: {
|
|
label: 'New Message',
|
|
pillClass: 'border-blue-500/30 bg-blue-500/10 text-blue-300',
|
|
},
|
|
first_inbound_message: {
|
|
label: 'First Message from Contact',
|
|
pillClass: 'border-teal-500/30 bg-teal-500/10 text-teal-300',
|
|
},
|
|
keyword_match: {
|
|
label: 'Keyword Match',
|
|
pillClass: 'border-purple-500/30 bg-purple-500/10 text-purple-300',
|
|
},
|
|
new_contact_created: {
|
|
label: 'New Contact',
|
|
pillClass: 'border-primary/30 bg-primary/10 text-primary',
|
|
},
|
|
conversation_assigned: {
|
|
label: 'Conversation Assigned',
|
|
pillClass: 'border-cyan-500/30 bg-cyan-500/10 text-cyan-300',
|
|
},
|
|
tag_added: {
|
|
label: 'Tag Added',
|
|
pillClass: 'border-amber-500/30 bg-amber-500/10 text-amber-300',
|
|
},
|
|
time_based: {
|
|
label: 'Time-Based',
|
|
pillClass: 'border-slate-500/30 bg-slate-500/10 text-muted-foreground',
|
|
},
|
|
interactive_reply: {
|
|
label: 'Button / List Reply',
|
|
pillClass: 'border-pink-500/30 bg-pink-500/10 text-pink-300',
|
|
},
|
|
}
|
|
|
|
export function triggerMeta(t: AutomationTriggerType | string): TriggerMeta {
|
|
return (
|
|
TRIGGER_META[t as AutomationTriggerType] ?? {
|
|
label: t,
|
|
pillClass: 'border-slate-500/30 bg-slate-500/10 text-muted-foreground',
|
|
}
|
|
)
|
|
}
|
|
|
|
export function formatRelative(iso: string | null | undefined): string {
|
|
if (!iso) return 'never'
|
|
const then = new Date(iso).getTime()
|
|
if (Number.isNaN(then)) return 'never'
|
|
const diffSec = Math.round((Date.now() - then) / 1000)
|
|
if (diffSec < 60) return 'just now'
|
|
if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`
|
|
if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`
|
|
if (diffSec < 2_592_000) return `${Math.floor(diffSec / 86400)}d ago`
|
|
return new Date(iso).toLocaleDateString()
|
|
}
|