@fayz-ai/plugin-conversations 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/InboxWatcher.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.js +159 -23
- package/dist/index.js.map +1 -1
- package/dist/lib/campainha.d.ts +31 -0
- package/dist/lib/campainha.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/MessageThread.d.ts.map +1 -1
- package/dist/views/useCaixaAoVivo.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InboxWatcher.d.ts","sourceRoot":"","sources":["../../src/components/InboxWatcher.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"InboxWatcher.d.ts","sourceRoot":"","sources":["../../src/components/InboxWatcher.tsx"],"names":[],"mappings":"AAkGA,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,KAAwB,EAAE,OAAgB,EAAE,QAAY,EAAE,EAAE;IAChG,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;qCAEiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,QAiFA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAwBpD,MAAM,WAAW,2BAA2B;IAC1C,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC9C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAwBpD,MAAM,WAAW,2BAA2B;IAC1C,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC9C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAmDD,wBAAgB,mCAAmC,CACjD,MAAM,CAAC,EAAE,2BAA2B,GACnC,qBAAqB,CAuRvB"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React11 from 'react';
|
|
2
2
|
import React11__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
-
import { Loader2, MessageSquare, AlertTriangle, Pause, Play, Send, RefreshCw, Plus, Clock, CheckCircle2, Check, ShieldCheck, ShieldAlert, QrCode, Zap, ExternalLink, SquarePen, Globe, Mail, Instagram, Phone, Search, User, Inbox, CheckCheck, ChevronLeft, Archive, PanelRight, X, MapPin, Tag, StickyNote, Link2, MoreVertical, Snowflake, Sun, Flame, Building2 } from 'lucide-react';
|
|
3
|
+
import { Loader2, MessageSquare, AlertTriangle, Pause, Play, Send, RefreshCw, Plus, Clock, CheckCircle2, Check, ShieldCheck, ShieldAlert, QrCode, Zap, ExternalLink, SquarePen, Globe, Mail, Instagram, Phone, Search, User, Inbox, CheckCheck, ChevronLeft, Archive, PanelRight, X, MapPin, Tag, StickyNote, Link2, MoreVertical, Snowflake, Sun, Flame, Building2, UserRound, Copy } from 'lucide-react';
|
|
4
4
|
import { toast, Button, Input, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, FilterChip, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
|
|
5
5
|
import { createConnectionStore, connectionRuns, connectionStatus, getActiveTenantId, getSupabaseClientOptional, useActiveTenantId, registerTranslations, useTranslation, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, errorMessage } from '@fayz-ai/core';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -386,6 +386,37 @@ var T = {
|
|
|
386
386
|
messages: "plg_conversation_messages",
|
|
387
387
|
channels: "plg_conversations_channels"
|
|
388
388
|
};
|
|
389
|
+
var porConta = /* @__PURE__ */ new Map();
|
|
390
|
+
function assinarChegada(tenantId, ouvinte) {
|
|
391
|
+
const supabase = getSupabaseClientOptional();
|
|
392
|
+
if (!supabase?.channel || !tenantId) return () => {
|
|
393
|
+
};
|
|
394
|
+
let assinatura = porConta.get(tenantId);
|
|
395
|
+
if (!assinatura) {
|
|
396
|
+
const ouvintes = /* @__PURE__ */ new Set();
|
|
397
|
+
const canal = supabase.channel(`conversas:${tenantId}`).on("broadcast", { event: "inbound" }, (msg2) => {
|
|
398
|
+
const carga = { conversationId: msg2?.payload?.conversationId };
|
|
399
|
+
for (const f of [...ouvintes]) {
|
|
400
|
+
try {
|
|
401
|
+
f(carga);
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}).subscribe();
|
|
406
|
+
assinatura = { canal, ouvintes };
|
|
407
|
+
porConta.set(tenantId, assinatura);
|
|
408
|
+
}
|
|
409
|
+
assinatura.ouvintes.add(ouvinte);
|
|
410
|
+
return () => {
|
|
411
|
+
const a = porConta.get(tenantId);
|
|
412
|
+
if (!a) return;
|
|
413
|
+
a.ouvintes.delete(ouvinte);
|
|
414
|
+
if (a.ouvintes.size === 0) {
|
|
415
|
+
porConta.delete(tenantId);
|
|
416
|
+
void getSupabaseClientOptional()?.removeChannel?.(a.canal);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
389
420
|
|
|
390
421
|
// src/types.ts
|
|
391
422
|
var CHANNEL_LABELS = {
|
|
@@ -484,26 +515,22 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
|
|
|
484
515
|
void contar();
|
|
485
516
|
}, [contar]);
|
|
486
517
|
React11.useEffect(() => {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const canal = supabase.channel(`conversas:${tenantId}`).on("broadcast", { event: "inbound" }, (msg2) => {
|
|
490
|
-
const id = msg2?.payload?.conversationId;
|
|
518
|
+
if (!tenantId) return;
|
|
519
|
+
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
491
520
|
void contar();
|
|
492
521
|
if (podeTocar.current) tocar();
|
|
493
522
|
void (async () => {
|
|
523
|
+
const supabase = getSupabaseClientOptional();
|
|
494
524
|
let titulo = label;
|
|
495
525
|
let corpo = "";
|
|
496
|
-
if (
|
|
497
|
-
const { data } = await supabase.from(T.conversations).select("contact_name, last_message_preview").eq("id",
|
|
526
|
+
if (conversationId && supabase) {
|
|
527
|
+
const { data } = await supabase.from(T.conversations).select("contact_name, last_message_preview").eq("id", conversationId).maybeSingle();
|
|
498
528
|
titulo = data?.contact_name || label;
|
|
499
529
|
corpo = String(data?.last_message_preview || "").replace(/\s+/g, " ").slice(0, 140);
|
|
500
530
|
}
|
|
501
|
-
avisarNoSistema(titulo, corpo, String(
|
|
531
|
+
avisarNoSistema(titulo, corpo, String(conversationId ?? "inbox"));
|
|
502
532
|
})();
|
|
503
|
-
})
|
|
504
|
-
return () => {
|
|
505
|
-
void supabase.removeChannel?.(canal);
|
|
506
|
-
};
|
|
533
|
+
});
|
|
507
534
|
}, [contar, tenantId, label]);
|
|
508
535
|
useNavContribution("conversations:unread", naoLidas > 0 ? [{ id: "conversations", label, route, badge: naoLidas, badgeTone: "destructive", section, position }] : []);
|
|
509
536
|
return null;
|
|
@@ -1172,6 +1199,51 @@ function MessageMenu({ m }) {
|
|
|
1172
1199
|
] }) })
|
|
1173
1200
|
] });
|
|
1174
1201
|
}
|
|
1202
|
+
function ContactCard({ contacts, outbound }) {
|
|
1203
|
+
const [copied, setCopied] = React11__default.useState(null);
|
|
1204
|
+
const copy = (value) => {
|
|
1205
|
+
void navigator.clipboard?.writeText(value);
|
|
1206
|
+
setCopied(value);
|
|
1207
|
+
window.setTimeout(() => setCopied((c) => c === value ? null : c), 1500);
|
|
1208
|
+
};
|
|
1209
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: contacts.map((c, i) => /* @__PURE__ */ jsxs(
|
|
1210
|
+
"div",
|
|
1211
|
+
{
|
|
1212
|
+
className: cn(
|
|
1213
|
+
"flex items-start gap-2.5 rounded-xl px-2.5 py-2",
|
|
1214
|
+
outbound ? "bg-white/15" : "bg-muted"
|
|
1215
|
+
),
|
|
1216
|
+
children: [
|
|
1217
|
+
/* @__PURE__ */ jsx(UserRound, { className: cn("mt-0.5 h-4 w-4 shrink-0", outbound ? "text-white/80" : "text-muted-foreground") }),
|
|
1218
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1219
|
+
/* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: c.name }),
|
|
1220
|
+
c.phones.map((p) => /* @__PURE__ */ jsxs(
|
|
1221
|
+
"button",
|
|
1222
|
+
{
|
|
1223
|
+
type: "button",
|
|
1224
|
+
onClick: () => copy(p.display),
|
|
1225
|
+
className: cn(
|
|
1226
|
+
"group/phone mt-0.5 flex items-center gap-1.5 text-left text-[13px] tabular-nums",
|
|
1227
|
+
outbound ? "text-white/90 hover:text-white" : "text-muted-foreground hover:text-foreground"
|
|
1228
|
+
),
|
|
1229
|
+
children: [
|
|
1230
|
+
p.display,
|
|
1231
|
+
copied === p.display ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3 opacity-0 transition-opacity group-hover/phone:opacity-100" })
|
|
1232
|
+
]
|
|
1233
|
+
},
|
|
1234
|
+
p.digits
|
|
1235
|
+
))
|
|
1236
|
+
] })
|
|
1237
|
+
]
|
|
1238
|
+
},
|
|
1239
|
+
`${c.name}-${i}`
|
|
1240
|
+
)) });
|
|
1241
|
+
}
|
|
1242
|
+
function MessageBody({ m, outbound }) {
|
|
1243
|
+
const contacts = m.messageType === "contact" ? m.payload?.contacts ?? [] : [];
|
|
1244
|
+
if (contacts.length > 0) return /* @__PURE__ */ jsx(ContactCard, { contacts, outbound });
|
|
1245
|
+
return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: m.body });
|
|
1246
|
+
}
|
|
1175
1247
|
function buildRows(messages) {
|
|
1176
1248
|
const rows = [];
|
|
1177
1249
|
let lastDay = "";
|
|
@@ -1290,7 +1362,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
|
|
|
1290
1362
|
),
|
|
1291
1363
|
style: outbound ? { backgroundColor: failed ? "hsl(var(--destructive))" : accent.color } : void 0,
|
|
1292
1364
|
children: [
|
|
1293
|
-
/* @__PURE__ */ jsx(
|
|
1365
|
+
/* @__PURE__ */ jsx(MessageBody, { m, outbound }),
|
|
1294
1366
|
/* @__PURE__ */ jsxs("div", { className: cn("mt-0.5 flex items-center justify-end gap-1 text-[10px]", outbound ? "text-white/70" : "text-muted-foreground"), children: [
|
|
1295
1367
|
new Date(m.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
1296
1368
|
/* @__PURE__ */ jsx(DeliveryMark, { m })
|
|
@@ -1637,17 +1709,13 @@ function useCaixaAoVivo() {
|
|
|
1637
1709
|
const abertoRef = React11.useRef(selectedId);
|
|
1638
1710
|
abertoRef.current = selectedId;
|
|
1639
1711
|
React11.useEffect(() => {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const canal = supabase.channel(`conversas:${tenantId}`).on("broadcast", { event: "inbound" }, (msg2) => {
|
|
1712
|
+
if (!tenantId) return;
|
|
1713
|
+
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
1643
1714
|
void load();
|
|
1644
|
-
if (
|
|
1715
|
+
if (conversationId && conversationId === abertoRef.current) {
|
|
1645
1716
|
void refreshMessages();
|
|
1646
1717
|
}
|
|
1647
|
-
})
|
|
1648
|
-
return () => {
|
|
1649
|
-
void supabase.removeChannel?.(canal);
|
|
1650
|
-
};
|
|
1718
|
+
});
|
|
1651
1719
|
}, [tenantId, load, refreshMessages]);
|
|
1652
1720
|
}
|
|
1653
1721
|
function InboxView() {
|
|
@@ -2055,7 +2123,12 @@ function mapMessage(r) {
|
|
|
2055
2123
|
at: r.at ?? "",
|
|
2056
2124
|
author: r.author ?? "",
|
|
2057
2125
|
deliveryStatus: r.delivery_status ?? null,
|
|
2058
|
-
providerMessageId: r.provider_message_id ?? null
|
|
2126
|
+
providerMessageId: r.provider_message_id ?? null,
|
|
2127
|
+
// NULL stays null rather than defaulting to 'text'. A row written before
|
|
2128
|
+
// migration 012 genuinely does not know what it was, and the rows that most
|
|
2129
|
+
// need saying so are the empty ones that a 'text' default would call fine.
|
|
2130
|
+
messageType: r.message_type ?? null,
|
|
2131
|
+
payload: r.payload ?? null
|
|
2059
2132
|
};
|
|
2060
2133
|
}
|
|
2061
2134
|
function createSupabaseConversationsProvider(config) {
|
|
@@ -5460,6 +5533,68 @@ END $$;
|
|
|
5460
5533
|
COMMENT ON FUNCTION public.plg_conversations_anunciar_chegada() IS
|
|
5461
5534
|
'Toca a campainha no t\xF3pico \`conversas:<tenant>\` \u2014 s\xF3 o id da conversa. O conte\xFAdo \xE9 lido da tabela, com RLS (011).';
|
|
5462
5535
|
`;
|
|
5536
|
+
var MIGRATION_012_A_RICH_MESSAGE_KEEPS_ITS_SHAPE = `-- ---------------------------------------------------------------------------
|
|
5537
|
+
-- 012_a_rich_message_keeps_its_shape.sql
|
|
5538
|
+
--
|
|
5539
|
+
-- \u2500\u2500 what was lost, and how \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5540
|
+
--
|
|
5541
|
+
-- \`plg_conversation_messages\` has exactly one content column: \`body text\`. A
|
|
5542
|
+
-- WhatsApp message that is not text therefore arrives flattened to whatever
|
|
5543
|
+
-- string the ingest could make of it \u2014 and when the ingest could make none, to
|
|
5544
|
+
-- the empty string, which renders as an empty bubble.
|
|
5545
|
+
--
|
|
5546
|
+
-- On 2026-09-22 a lead sent his finance director's contact card. It stored as
|
|
5547
|
+
-- \`body = ''\`. He then sent a screenshot of the same card; that stored as
|
|
5548
|
+
-- \`body = ''\` too. The fact the conversation existed to obtain \u2014 a name and a
|
|
5549
|
+
-- phone number \u2014 was in the payload both times and in the row neither time.
|
|
5550
|
+
--
|
|
5551
|
+
-- Recovering it meant going back to the Evolution instance and reading the
|
|
5552
|
+
-- provider's own copy. That worked, but it is not a recovery path: Evolution
|
|
5553
|
+
-- prunes, the instance is local-only, and a row that needs a second system to
|
|
5554
|
+
-- be legible is not a record.
|
|
5555
|
+
--
|
|
5556
|
+
-- \u2500\u2500 the two columns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5557
|
+
--
|
|
5558
|
+
-- \`message_type\` names what the provider sent \u2014 text, contact, image, audio,
|
|
5559
|
+
-- document, sticker, location, reaction, unknown. The inbox needs it to choose
|
|
5560
|
+
-- a bubble, and \`unknown\` is the one that makes the NEXT gap visible in a query
|
|
5561
|
+
-- instead of in a complaint.
|
|
5562
|
+
--
|
|
5563
|
+
-- \`payload\` keeps the part that does not survive flattening: the vCard's parsed
|
|
5564
|
+
-- name and numbers, a media mimetype and size, a location's coordinates. Not
|
|
5565
|
+
-- the whole provider envelope \u2014 that carries decryption keys and a base64
|
|
5566
|
+
-- thumbnail, neither of which belongs in a row that every member of the tenant
|
|
5567
|
+
-- can read.
|
|
5568
|
+
--
|
|
5569
|
+
-- Deliberately NOT stored here: delivery status. It changes after the fact \u2014
|
|
5570
|
+
-- sent, delivered, read \u2014 and \`conversations-provider\`'s \`message_detail\` reads
|
|
5571
|
+
-- it live for that reason. \`payload\` is the shape of the message, which does
|
|
5572
|
+
-- not change once it arrives.
|
|
5573
|
+
--
|
|
5574
|
+
-- \u2500\u2500 why both are nullable, and nothing is backfilled \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5575
|
+
--
|
|
5576
|
+
-- Every row written before this migration has neither, and no default can
|
|
5577
|
+
-- invent them: \`message_type\` would have to guess, and guessing 'text' for the
|
|
5578
|
+
-- three empty bubbles already in the table would assert something false about
|
|
5579
|
+
-- exactly the rows this exists to fix. A reader that treats NULL as "written
|
|
5580
|
+
-- before 012" is correct; a reader that treats '' as text is the bug.
|
|
5581
|
+
--
|
|
5582
|
+
-- Table-level \`GRANT ALL ... TO authenticated\` already covers new columns, and
|
|
5583
|
+
-- the four tenant policies are row-scoped, so neither needs restating.
|
|
5584
|
+
--
|
|
5585
|
+
-- Idempotent and replay-safe.
|
|
5586
|
+
-- ---------------------------------------------------------------------------
|
|
5587
|
+
|
|
5588
|
+
ALTER TABLE public.plg_conversation_messages
|
|
5589
|
+
ADD COLUMN IF NOT EXISTS message_type text,
|
|
5590
|
+
ADD COLUMN IF NOT EXISTS payload jsonb;
|
|
5591
|
+
|
|
5592
|
+
COMMENT ON COLUMN public.plg_conversation_messages.message_type IS
|
|
5593
|
+
'What the provider sent: text, contact, image, video, audio, document, sticker, location, reaction, unknown. NULL means the row predates 012 (#504).';
|
|
5594
|
+
|
|
5595
|
+
COMMENT ON COLUMN public.plg_conversation_messages.payload IS
|
|
5596
|
+
'The structured remainder that \`body\` cannot hold \u2014 parsed vCards, media metadata, coordinates. Never credentials, thumbnails or decryption keys; never delivery status, which is read live (#504).';
|
|
5597
|
+
`;
|
|
5463
5598
|
var MIGRATIONS = [
|
|
5464
5599
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
5465
5600
|
{ id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO },
|
|
@@ -5472,7 +5607,8 @@ var MIGRATIONS = [
|
|
|
5472
5607
|
{ id: "008_dar_por_vista_e_uma_decisao", sql: MIGRATION_008_DAR_POR_VISTA_E_UMA_DECISAO },
|
|
5473
5608
|
{ id: "009_a_mensagem_inteira_atravessa_o_realtime", sql: MIGRATION_009_A_MENSAGEM_INTEIRA_ATRAVESSA_O_REALTIME },
|
|
5474
5609
|
{ id: "010_a_chegada_e_anunciada_por_conta", sql: MIGRATION_010_A_CHEGADA_E_ANUNCIADA_POR_CONTA },
|
|
5475
|
-
{ id: "011_o_anuncio_e_uma_campainha_nao_um_recado", sql: MIGRATION_011_O_ANUNCIO_E_UMA_CAMPAINHA_NAO_UM_RECADO }
|
|
5610
|
+
{ id: "011_o_anuncio_e_uma_campainha_nao_um_recado", sql: MIGRATION_011_O_ANUNCIO_E_UMA_CAMPAINHA_NAO_UM_RECADO },
|
|
5611
|
+
{ id: "012_a_rich_message_keeps_its_shape", sql: MIGRATION_012_A_RICH_MESSAGE_KEEPS_ITS_SHAPE }
|
|
5476
5612
|
];
|
|
5477
5613
|
|
|
5478
5614
|
// src/index.ts
|