@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.
@@ -1 +1 @@
1
- {"version":3,"file":"InboxWatcher.d.ts","sourceRoot":"","sources":["../../src/components/InboxWatcher.tsx"],"names":[],"mappings":"AAiGA,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,QAgGA"}
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;AA8CD,wBAAgB,mCAAmC,CACjD,MAAM,CAAC,EAAE,2BAA2B,GACnC,qBAAqB,CAuRvB"}
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
- const supabase = getSupabaseClientOptional();
488
- if (!supabase?.channel || !tenantId) return;
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 (id) {
497
- const { data } = await supabase.from(T.conversations).select("contact_name, last_message_preview").eq("id", id).maybeSingle();
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(id ?? "inbox"));
531
+ avisarNoSistema(titulo, corpo, String(conversationId ?? "inbox"));
502
532
  })();
503
- }).subscribe();
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("p", { className: "whitespace-pre-wrap break-words", children: m.body }),
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
- const supabase = getSupabaseClientOptional();
1641
- if (!supabase?.channel || !tenantId) return;
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 (msg2?.payload?.conversationId && msg2.payload.conversationId === abertoRef.current) {
1715
+ if (conversationId && conversationId === abertoRef.current) {
1645
1716
  void refreshMessages();
1646
1717
  }
1647
- }).subscribe();
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