@fayz-ai/plugin-conversations 0.18.0 → 0.19.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":"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"}
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;AAoDD,wBAAgB,mCAAmC,CACjD,MAAM,CAAC,EAAE,2BAA2B,GACnC,qBAAqB,CAuRvB"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import * as React11 from 'react';
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, UserRound, Copy } from 'lucide-react';
1
+ import * as React12 from 'react';
2
+ import React12__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, UserRound, Copy, FileText, Download } 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';
@@ -211,12 +211,12 @@ function Bar({ c }) {
211
211
  ] });
212
212
  }
213
213
  function CampaignsView() {
214
- const [rows, setRows] = React11__default.useState(null);
215
- const [templates, setTemplates] = React11__default.useState([]);
216
- const [providers, setProviders] = React11__default.useState([]);
217
- const [running, setRunning] = React11__default.useState(null);
218
- const stop = React11__default.useRef(false);
219
- const load = React11__default.useCallback(async () => {
214
+ const [rows, setRows] = React12__default.useState(null);
215
+ const [templates, setTemplates] = React12__default.useState([]);
216
+ const [providers, setProviders] = React12__default.useState([]);
217
+ const [running, setRunning] = React12__default.useState(null);
218
+ const stop = React12__default.useRef(false);
219
+ const load = React12__default.useCallback(async () => {
220
220
  try {
221
221
  const [c, t, p] = await Promise.all([
222
222
  campaignsApi.list(),
@@ -231,7 +231,7 @@ function CampaignsView() {
231
231
  setRows([]);
232
232
  }
233
233
  }, []);
234
- React11__default.useEffect(() => {
234
+ React12__default.useEffect(() => {
235
235
  void load();
236
236
  }, [load]);
237
237
  const active = providers.find((p) => p.isActive);
@@ -485,9 +485,9 @@ function avisarNoSistema(titulo, corpo, tag) {
485
485
  }
486
486
  function InboxWatcher({ label, route = "/conversations", section = "main", position = 1 }) {
487
487
  const tenantId = useActiveTenantId();
488
- const [naoLidas, setNaoLidas] = React11.useState(0);
489
- const podeTocar = React11.useRef(false);
490
- React11.useEffect(() => {
488
+ const [naoLidas, setNaoLidas] = React12.useState(0);
489
+ const podeTocar = React12.useRef(false);
490
+ React12.useEffect(() => {
491
491
  const liberar = () => {
492
492
  podeTocar.current = true;
493
493
  void pedirPermissao();
@@ -499,7 +499,7 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
499
499
  for (const ev of ["pointerdown", "keydown", "touchstart"]) window.removeEventListener(ev, liberar);
500
500
  };
501
501
  }, []);
502
- const contar = React11.useCallback(async () => {
502
+ const contar = React12.useCallback(async () => {
503
503
  const supabase = getSupabaseClientOptional();
504
504
  if (!supabase) return;
505
505
  const { data } = await supabase.from(T.conversations).select("id, status, last_message_direction, unread_count").eq("status", "open");
@@ -511,10 +511,10 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
511
511
  })).length;
512
512
  setNaoLidas(n);
513
513
  }, []);
514
- React11.useEffect(() => {
514
+ React12.useEffect(() => {
515
515
  void contar();
516
516
  }, [contar]);
517
- React11.useEffect(() => {
517
+ React12.useEffect(() => {
518
518
  if (!tenantId) return;
519
519
  return assinarChegada(tenantId, ({ conversationId }) => {
520
520
  void contar();
@@ -549,14 +549,14 @@ function slots(body) {
549
549
  return found.size;
550
550
  }
551
551
  function TemplatesView() {
552
- const [mine, setMine] = React11__default.useState(null);
553
- const [theirs, setTheirs] = React11__default.useState([]);
554
- const [providers, setProviders] = React11__default.useState([]);
555
- const [busy, setBusy] = React11__default.useState(null);
556
- const [draft, setDraft] = React11__default.useState({ name: "", body: "", variables: "" });
552
+ const [mine, setMine] = React12__default.useState(null);
553
+ const [theirs, setTheirs] = React12__default.useState([]);
554
+ const [providers, setProviders] = React12__default.useState([]);
555
+ const [busy, setBusy] = React12__default.useState(null);
556
+ const [draft, setDraft] = React12__default.useState({ name: "", body: "", variables: "" });
557
557
  const active = providers.find((p) => p.isActive);
558
558
  const supportsTemplates = active?.capabilities?.templates !== false;
559
- const load = React11__default.useCallback(async () => {
559
+ const load = React12__default.useCallback(async () => {
560
560
  try {
561
561
  const [m, p] = await Promise.all([campaignsApi.templates(), providersApi.list()]);
562
562
  setMine(m);
@@ -566,7 +566,7 @@ function TemplatesView() {
566
566
  setMine([]);
567
567
  }
568
568
  }, []);
569
- React11__default.useEffect(() => {
569
+ React12__default.useEffect(() => {
570
570
  void load();
571
571
  }, [load]);
572
572
  const sync = async () => {
@@ -757,8 +757,8 @@ function TemplatesView() {
757
757
  var DEFAULT_CONVERSATIONS_CONFIG = {
758
758
  contactKind: "contact"
759
759
  };
760
- var StoreContext = React11__default.createContext(null);
761
- var ConfigContext = React11__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
760
+ var StoreContext = React12__default.createContext(null);
761
+ var ConfigContext = React12__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
762
762
  function ConversationsContextProvider({
763
763
  store: store2,
764
764
  config = DEFAULT_CONVERSATIONS_CONFIG,
@@ -767,12 +767,12 @@ function ConversationsContextProvider({
767
767
  return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store2, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
768
768
  }
769
769
  function useConversationsStore(selector) {
770
- const store2 = React11__default.useContext(StoreContext);
770
+ const store2 = React12__default.useContext(StoreContext);
771
771
  if (!store2) throw new Error("useConversationsStore must be used within ConversationsPage");
772
772
  return useStore(store2, selector);
773
773
  }
774
774
  function useConversationsConfig() {
775
- return React11__default.useContext(ConfigContext);
775
+ return React12__default.useContext(ConfigContext);
776
776
  }
777
777
  var CHANNEL_ICON = {
778
778
  sms: Phone,
@@ -789,10 +789,10 @@ var CHANNEL_ACCENT = {
789
789
  webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
790
790
  };
791
791
  function useMediaQuery(query) {
792
- const [matches, setMatches] = React11__default.useState(
792
+ const [matches, setMatches] = React12__default.useState(
793
793
  () => typeof window !== "undefined" && window.matchMedia(query).matches
794
794
  );
795
- React11__default.useEffect(() => {
795
+ React12__default.useEffect(() => {
796
796
  if (typeof window === "undefined") return;
797
797
  const mql = window.matchMedia(query);
798
798
  const onChange = () => setMatches(mql.matches);
@@ -881,7 +881,7 @@ function ConversationList({ className }) {
881
881
  setLeadOwnerFilter,
882
882
  setSearch
883
883
  } = useConversationsStore((s) => s);
884
- const donos = React11__default.useMemo(() => {
884
+ const donos = React12__default.useMemo(() => {
885
885
  const vistos = /* @__PURE__ */ new Map();
886
886
  for (const c of conversations) {
887
887
  if (c.leadOwnerId) vistos.set(c.leadOwnerId, c.leadOwnerName ?? c.leadOwnerId);
@@ -1016,9 +1016,9 @@ function ConversationList({ className }) {
1016
1016
  ] });
1017
1017
  }
1018
1018
  function useSendWindow(conversationId) {
1019
- const [row, setRow] = React11__default.useState(null);
1020
- const [providers, setProviders] = React11__default.useState(null);
1021
- React11__default.useEffect(() => {
1019
+ const [row, setRow] = React12__default.useState(null);
1020
+ const [providers, setProviders] = React12__default.useState(null);
1021
+ React12__default.useEffect(() => {
1022
1022
  let alive = true;
1023
1023
  providersApi.list().then((p) => {
1024
1024
  if (alive) setProviders(p);
@@ -1029,7 +1029,7 @@ function useSendWindow(conversationId) {
1029
1029
  alive = false;
1030
1030
  };
1031
1031
  }, []);
1032
- React11__default.useEffect(() => {
1032
+ React12__default.useEffect(() => {
1033
1033
  let alive = true;
1034
1034
  if (!conversationId) {
1035
1035
  setRow(null);
@@ -1050,7 +1050,7 @@ function useSendWindow(conversationId) {
1050
1050
  };
1051
1051
  }, [conversationId]);
1052
1052
  const provider = (providers ?? []).find((p) => p.isActive);
1053
- const mode = React11__default.useMemo(() => {
1053
+ const mode = React12__default.useMemo(() => {
1054
1054
  if (!provider) return { kind: "blocked", reason: "no_provider" };
1055
1055
  if (!provider.capabilities?.requires_template) {
1056
1056
  return { kind: "free", until: null, minutesLeft: null };
@@ -1125,10 +1125,10 @@ function DeliveryMark({ m }) {
1125
1125
  ] });
1126
1126
  }
1127
1127
  function MessageMenu({ m }) {
1128
- const [open, setOpen] = React11__default.useState(false);
1129
- const [detail, setDetail] = React11__default.useState(null);
1130
- const [loading, setLoading] = React11__default.useState(false);
1131
- const [err, setErr] = React11__default.useState(null);
1128
+ const [open, setOpen] = React12__default.useState(false);
1129
+ const [detail, setDetail] = React12__default.useState(null);
1130
+ const [loading, setLoading] = React12__default.useState(false);
1131
+ const [err, setErr] = React12__default.useState(null);
1132
1132
  async function load() {
1133
1133
  setOpen(true);
1134
1134
  if (!m.providerMessageId) return;
@@ -1199,8 +1199,136 @@ function MessageMenu({ m }) {
1199
1199
  ] }) })
1200
1200
  ] });
1201
1201
  }
1202
+ var COM_ARQUIVO = /* @__PURE__ */ new Set(["image", "video", "audio", "document", "sticker"]);
1203
+ function temArquivo(m) {
1204
+ return !!m.mediaPath && COM_ARQUIVO.has(String(m.messageType));
1205
+ }
1206
+ function useUrlAssinada(m, visivel) {
1207
+ const [url, setUrl] = React12.useState(null);
1208
+ const [erro, setErro] = React12.useState(false);
1209
+ React12.useEffect(() => {
1210
+ if (!visivel || url || erro || !temArquivo(m)) return;
1211
+ let vivo = true;
1212
+ (async () => {
1213
+ try {
1214
+ const sb = getSupabaseClientOptional();
1215
+ const { data, error } = await sb.functions.invoke("conversations-provider", {
1216
+ body: { tenantId: getActiveTenantId(), action: "media_url", messageId: m.id }
1217
+ });
1218
+ if (error || data?.error) throw new Error("falhou");
1219
+ if (vivo && data?.url) setUrl(data.url);
1220
+ else if (vivo) setErro(true);
1221
+ } catch {
1222
+ if (vivo) setErro(true);
1223
+ }
1224
+ })();
1225
+ return () => {
1226
+ vivo = false;
1227
+ };
1228
+ }, [visivel, m.id, url, erro]);
1229
+ return { url, erro };
1230
+ }
1231
+ function tamanho(bytes) {
1232
+ if (!bytes || bytes <= 0) return "";
1233
+ if (bytes < 1024) return `${bytes} B`;
1234
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
1235
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1236
+ }
1237
+ function duracao(seg) {
1238
+ if (!seg || seg <= 0) return "";
1239
+ const m = Math.floor(seg / 60);
1240
+ const s = Math.floor(seg % 60);
1241
+ return `${m}:${String(s).padStart(2, "0")}`;
1242
+ }
1243
+ function MediaBubble({ m, outbound }) {
1244
+ const ref = React12.useRef(null);
1245
+ const [visivel, setVisivel] = React12.useState(false);
1246
+ const { url, erro } = useUrlAssinada(m, visivel);
1247
+ React12.useEffect(() => {
1248
+ const el = ref.current;
1249
+ if (!el || typeof IntersectionObserver === "undefined") {
1250
+ setVisivel(true);
1251
+ return;
1252
+ }
1253
+ const obs = new IntersectionObserver((entradas) => {
1254
+ if (entradas.some((e) => e.isIntersecting)) {
1255
+ setVisivel(true);
1256
+ obs.disconnect();
1257
+ }
1258
+ }, { rootMargin: "200px" });
1259
+ obs.observe(el);
1260
+ return () => obs.disconnect();
1261
+ }, []);
1262
+ const tipo = String(m.messageType);
1263
+ const p = m.payload ?? {};
1264
+ const legenda = p.caption ?? null;
1265
+ const rotulo = /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: m.body });
1266
+ if (erro) {
1267
+ return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
1268
+ rotulo,
1269
+ /* @__PURE__ */ jsx("p", { className: "text-xs opacity-60", children: "O arquivo n\xE3o carregou." })
1270
+ ] });
1271
+ }
1272
+ if (!url) {
1273
+ return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
1274
+ rotulo,
1275
+ /* @__PURE__ */ jsx("div", { className: "h-1 w-24 animate-pulse rounded bg-current opacity-20" })
1276
+ ] });
1277
+ }
1278
+ if (tipo === "image" || tipo === "sticker") {
1279
+ return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
1280
+ /* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsx(
1281
+ "img",
1282
+ {
1283
+ src: url,
1284
+ alt: legenda ?? (tipo === "sticker" ? "Figurinha" : "Imagem recebida"),
1285
+ className: tipo === "sticker" ? "max-h-32 w-auto" : "max-h-80 w-auto rounded-lg",
1286
+ loading: "lazy"
1287
+ }
1288
+ ) }),
1289
+ legenda ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: legenda }) : null
1290
+ ] });
1291
+ }
1292
+ if (tipo === "video") {
1293
+ return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
1294
+ /* @__PURE__ */ jsx("video", { src: url, controls: true, preload: "metadata", className: "max-h-80 w-auto rounded-lg" }),
1295
+ legenda ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: legenda }) : null
1296
+ ] });
1297
+ }
1298
+ if (tipo === "audio") {
1299
+ return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
1300
+ /* @__PURE__ */ jsx("audio", { src: url, controls: true, preload: "metadata", className: "h-9 w-64 max-w-full" }),
1301
+ /* @__PURE__ */ jsxs("p", { className: "text-xs opacity-60", children: [
1302
+ p.ptt ? "Mensagem de voz" : "\xC1udio",
1303
+ duracao(p.seconds) ? ` \xB7 ${duracao(p.seconds)}` : ""
1304
+ ] })
1305
+ ] });
1306
+ }
1307
+ const nome = p.fileName ?? "Documento";
1308
+ return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
1309
+ /* @__PURE__ */ jsxs(
1310
+ "a",
1311
+ {
1312
+ href: url,
1313
+ target: "_blank",
1314
+ rel: "noreferrer",
1315
+ download: nome,
1316
+ className: `flex items-center gap-2 rounded-lg border p-2 ${outbound ? "border-white/30" : "border-black/10"}`,
1317
+ children: [
1318
+ /* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 shrink-0 opacity-70" }),
1319
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
1320
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-sm", children: nome }),
1321
+ tamanho(p.fileLength) ? /* @__PURE__ */ jsx("span", { className: "block text-xs opacity-60", children: tamanho(p.fileLength) }) : null
1322
+ ] }),
1323
+ /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 shrink-0 opacity-70" })
1324
+ ]
1325
+ }
1326
+ ),
1327
+ legenda ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: legenda }) : null
1328
+ ] });
1329
+ }
1202
1330
  function ContactCard({ contacts, outbound }) {
1203
- const [copied, setCopied] = React11__default.useState(null);
1331
+ const [copied, setCopied] = React12__default.useState(null);
1204
1332
  const copy = (value) => {
1205
1333
  void navigator.clipboard?.writeText(value);
1206
1334
  setCopied(value);
@@ -1242,6 +1370,7 @@ function ContactCard({ contacts, outbound }) {
1242
1370
  function MessageBody({ m, outbound }) {
1243
1371
  const contacts = m.messageType === "contact" ? m.payload?.contacts ?? [] : [];
1244
1372
  if (contacts.length > 0) return /* @__PURE__ */ jsx(ContactCard, { contacts, outbound });
1373
+ if (temArquivo(m)) return /* @__PURE__ */ jsx(MediaBubble, { m, outbound });
1245
1374
  return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: m.body });
1246
1375
  }
1247
1376
  function buildRows(messages) {
@@ -1264,14 +1393,14 @@ function buildRows(messages) {
1264
1393
  function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
1265
1394
  const t = useTranslation();
1266
1395
  const { messages, sending, send, setStatus, refreshMessages } = useConversationsStore((s) => s);
1267
- const [draft, setDraft] = React11__default.useState("");
1396
+ const [draft, setDraft] = React12__default.useState("");
1268
1397
  const win = useSendWindow(selected.id);
1269
1398
  const canType = win.mode.kind === "free";
1270
- const threadRef = React11__default.useRef(null);
1271
- React11__default.useEffect(() => {
1399
+ const threadRef = React12__default.useRef(null);
1400
+ React12__default.useEffect(() => {
1272
1401
  threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
1273
1402
  }, [messages.length, selected.id]);
1274
- React11__default.useEffect(() => {
1403
+ React12__default.useEffect(() => {
1275
1404
  if (!selected.id) return;
1276
1405
  let timer = null;
1277
1406
  const start = () => {
@@ -1300,7 +1429,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
1300
1429
  await send(body);
1301
1430
  }
1302
1431
  const accent = CHANNEL_ACCENT[selected.channel];
1303
- const rows = React11__default.useMemo(() => buildRows(messages), [messages]);
1432
+ const rows = React12__default.useMemo(() => buildRows(messages), [messages]);
1304
1433
  return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
1305
1434
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border bg-card px-3 py-2.5 md:px-5", children: [
1306
1435
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
@@ -1412,8 +1541,8 @@ var TEMP = {
1412
1541
  cold: { icon: Snowflake, cls: "bg-info-soft text-info-soft-foreground", label: "Frio" }
1413
1542
  };
1414
1543
  function LeadCard({ personId, handle }) {
1415
- const [lead, setLead] = React11__default.useState(void 0);
1416
- React11__default.useEffect(() => {
1544
+ const [lead, setLead] = React12__default.useState(void 0);
1545
+ React12__default.useEffect(() => {
1417
1546
  let alive = true;
1418
1547
  const sb = getSupabaseClientOptional();
1419
1548
  const tenantId = getActiveTenantId();
@@ -1580,14 +1709,14 @@ function NewConversationPanel({
1580
1709
  const create = useConversationsStore((s) => s.create);
1581
1710
  const config = useConversationsConfig();
1582
1711
  const guardConversations = useLimitGuard("conversations_month");
1583
- const [channel, setChannel] = React11__default.useState("whatsapp");
1584
- const [contact, setContact] = React11__default.useState(null);
1585
- const [typedHandle, setTypedHandle] = React11__default.useState("");
1586
- const [creatingContact, setCreatingContact] = React11__default.useState(false);
1587
- const [firstMessage, setFirstMessage] = React11__default.useState("");
1588
- const [submitting, setSubmitting] = React11__default.useState(false);
1589
- const [pickerKey, setPickerKey] = React11__default.useState(0);
1590
- React11__default.useEffect(() => {
1712
+ const [channel, setChannel] = React12__default.useState("whatsapp");
1713
+ const [contact, setContact] = React12__default.useState(null);
1714
+ const [typedHandle, setTypedHandle] = React12__default.useState("");
1715
+ const [creatingContact, setCreatingContact] = React12__default.useState(false);
1716
+ const [firstMessage, setFirstMessage] = React12__default.useState("");
1717
+ const [submitting, setSubmitting] = React12__default.useState(false);
1718
+ const [pickerKey, setPickerKey] = React12__default.useState(0);
1719
+ React12__default.useEffect(() => {
1591
1720
  if (open) {
1592
1721
  setChannel("whatsapp");
1593
1722
  setContact(null);
@@ -1706,9 +1835,9 @@ function useCaixaAoVivo() {
1706
1835
  const load = useConversationsStore((s) => s.load);
1707
1836
  const refreshMessages = useConversationsStore((s) => s.refreshMessages);
1708
1837
  const selectedId = useConversationsStore((s) => s.selectedId);
1709
- const abertoRef = React11.useRef(selectedId);
1838
+ const abertoRef = React12.useRef(selectedId);
1710
1839
  abertoRef.current = selectedId;
1711
- React11.useEffect(() => {
1840
+ React12.useEffect(() => {
1712
1841
  if (!tenantId) return;
1713
1842
  return assinarChegada(tenantId, ({ conversationId }) => {
1714
1843
  void load();
@@ -1723,10 +1852,10 @@ function InboxView() {
1723
1852
  const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
1724
1853
  useCaixaAoVivo();
1725
1854
  const isWidePanel = useMediaQuery("(min-width: 1280px)");
1726
- const [panelOpen, setPanelOpen] = React11__default.useState(false);
1727
- const [newOpen, setNewOpen] = React11__default.useState(false);
1855
+ const [panelOpen, setPanelOpen] = React12__default.useState(false);
1856
+ const [newOpen, setNewOpen] = React12__default.useState(false);
1728
1857
  const selected = conversations.find((c) => c.id === selectedId) ?? null;
1729
- React11__default.useEffect(() => {
1858
+ React12__default.useEffect(() => {
1730
1859
  setPanelOpen(isWidePanel);
1731
1860
  }, [isWidePanel]);
1732
1861
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1799,7 +1928,7 @@ function requestedContact() {
1799
1928
  }
1800
1929
  }
1801
1930
  function ConversationsPage({ store: store2, config }) {
1802
- React11__default.useEffect(() => {
1931
+ React12__default.useEffect(() => {
1803
1932
  const wanted = requestedContact();
1804
1933
  if (!wanted) {
1805
1934
  void store2.getState().load();
@@ -2128,7 +2257,8 @@ function mapMessage(r) {
2128
2257
  // migration 012 genuinely does not know what it was, and the rows that most
2129
2258
  // need saying so are the empty ones that a 'text' default would call fine.
2130
2259
  messageType: r.message_type ?? null,
2131
- payload: r.payload ?? null
2260
+ payload: r.payload ?? null,
2261
+ mediaPath: r.media_path ?? null
2132
2262
  };
2133
2263
  }
2134
2264
  function createSupabaseConversationsProvider(config) {
@@ -2887,22 +3017,22 @@ function NumberRow({ n }) {
2887
3017
  ] });
2888
3018
  }
2889
3019
  function WhatsAppProviders() {
2890
- const [rows, setRows] = React11__default.useState(null);
2891
- const [draft, setDraft] = React11__default.useState({});
2892
- const [busy, setBusy] = React11__default.useState(null);
2893
- const [numbers, setNumbers] = React11__default.useState({});
2894
- const [qr, setQr] = React11__default.useState(null);
2895
- const load = React11__default.useCallback(async () => {
3020
+ const [rows, setRows] = React12__default.useState(null);
3021
+ const [draft, setDraft] = React12__default.useState({});
3022
+ const [busy, setBusy] = React12__default.useState(null);
3023
+ const [numbers, setNumbers] = React12__default.useState({});
3024
+ const [qr, setQr] = React12__default.useState(null);
3025
+ const load = React12__default.useCallback(async () => {
2896
3026
  try {
2897
3027
  setRows(await providersApi.list());
2898
3028
  } catch {
2899
3029
  setRows([]);
2900
3030
  }
2901
3031
  }, []);
2902
- React11__default.useEffect(() => {
3032
+ React12__default.useEffect(() => {
2903
3033
  void load();
2904
3034
  }, [load]);
2905
- const byId = React11__default.useMemo(
3035
+ const byId = React12__default.useMemo(
2906
3036
  () => Object.fromEntries((rows ?? []).map((r) => [r.provider, r])),
2907
3037
  [rows]
2908
3038
  );
@@ -3137,9 +3267,9 @@ function ChannelRow({ account }) {
3137
3267
  }
3138
3268
  function ConversationsGeneralSettings() {
3139
3269
  const t = useTranslation();
3140
- const [channels, setChannels] = React11.useState(null);
3141
- const [failed, setFailed] = React11.useState(false);
3142
- React11.useEffect(() => {
3270
+ const [channels, setChannels] = React12.useState(null);
3271
+ const [failed, setFailed] = React12.useState(false);
3272
+ React12.useEffect(() => {
3143
3273
  let cancelled = false;
3144
3274
  listMessagingChannels().then((rows) => {
3145
3275
  if (!cancelled) setChannels(rows);
@@ -5595,6 +5725,72 @@ COMMENT ON COLUMN public.plg_conversation_messages.message_type IS
5595
5725
  COMMENT ON COLUMN public.plg_conversation_messages.payload IS
5596
5726
  '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
5727
  `;
5728
+ var MIGRATION_013_A_MIDIA_FICA_CONOSCO = `-- ---------------------------------------------------------------------------
5729
+ -- 013_a_midia_fica_conosco.sql
5730
+ --
5731
+ -- \u2500\u2500 o que a 012 resolveu, e o que ela n\xE3o resolveu \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5732
+ --
5733
+ -- A 012 fez a mensagem que n\xE3o \xE9 texto parar de chegar como bolha vazia:
5734
+ -- \`message_type\` diz que \xE9 imagem, \`payload\` guarda mimetype, tamanho e
5735
+ -- dura\xE7\xE3o. Funcionou \u2014 um \xE1udio de 23 segundos entrou classificado como
5736
+ -- \`audio\`, \`ptt: true\`, \`audio/ogg; codecs=opus\`, 55.825 bytes.
5737
+ --
5738
+ -- E a tela mostra \`[\xE1udio]\`. Porque saber QUE existe um arquivo n\xE3o \xE9 ter o
5739
+ -- arquivo. Nada no banco aponta para bytes nenhuns: o WhatsApp entrega m\xEDdia
5740
+ -- cifrada num CDN pr\xF3prio, e quem decifra \xE9 o provedor.
5741
+ --
5742
+ -- \u2500\u2500 por que uma coluna, e n\xE3o um campo no payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5743
+ --
5744
+ -- A 012 escreveu que \`payload\` \xE9 "a forma da mensagem, que n\xE3o muda depois que
5745
+ -- ela chega". Onde o arquivo foi parar \xE9 decidido DEPOIS \u2014 no upload, que pode
5746
+ -- falhar, ser refeito, ou ainda n\xE3o ter acontecido. Enfiar isso no \`payload\`
5747
+ -- contradiz a \xFAnica frase que diz quando \`payload\` pode ser lido sem medo.
5748
+ --
5749
+ -- Em coluna, a pergunta que importa vira consulta:
5750
+ --
5751
+ -- SELECT count(*) FROM plg_conversation_messages
5752
+ -- WHERE message_type IN ('image','video','audio','document','sticker')
5753
+ -- AND media_path IS NULL;
5754
+ --
5755
+ -- "quanta m\xEDdia eu recebi e n\xE3o guardei" \u2014 que num campo de JSON seria uma
5756
+ -- varredura e aqui \xE9 um \xEDndice.
5757
+ --
5758
+ -- \u2500\u2500 por que guardar, em vez de buscar no provedor quando algu\xE9m abrir \u2500\u2500\u2500
5759
+ --
5760
+ -- A Evolution decifra sob demanda, e seria mais barato n\xE3o copiar nada. Mas ela
5761
+ -- \xE9 local-only, poda o que \xE9 antigo, e a inst\xE2ncia roda numa m\xE1quina que nem
5762
+ -- sempre est\xE1 de p\xE9. Essa era exatamente a obje\xE7\xE3o da 012 contra depender dela
5763
+ -- para ler um vCard: uma linha que precisa de um segundo sistema para ser
5764
+ -- leg\xEDvel n\xE3o \xE9 registro. Vale igual para os bytes.
5765
+ --
5766
+ -- O destino \xE9 o balde \`attachments\` \u2014 privado, 25 MB, j\xE1 existente e j\xE1 em uso.
5767
+ -- Uma foto que um lead mandou no WhatsApp n\xE3o pode ser p\xFAblica, e um balde novo
5768
+ -- seria mais uma lista de mimetypes para algu\xE9m manter desatualizada.
5769
+ --
5770
+ -- \u2500\u2500 nulo \xE9 um estado leg\xEDtimo, n\xE3o uma falha \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
5771
+ --
5772
+ -- \`media_path\` \xE9 NULL em tr\xEAs situa\xE7\xF5es diferentes e todas normais: a mensagem
5773
+ -- \xE9 texto; a linha \xE9 anterior a esta migra\xE7\xE3o; ou o upload ainda n\xE3o aconteceu.
5774
+ -- Quem l\xEA distingue pela combina\xE7\xE3o com \`message_type\`, que \xE9 o que torna a
5775
+ -- consulta acima poss\xEDvel. Um default inventaria um caminho para arquivo que
5776
+ -- n\xE3o existe.
5777
+ --
5778
+ -- Idempotente e replay-safe.
5779
+ -- ---------------------------------------------------------------------------
5780
+
5781
+ ALTER TABLE public.plg_conversation_messages
5782
+ ADD COLUMN IF NOT EXISTS media_path text;
5783
+
5784
+ COMMENT ON COLUMN public.plg_conversation_messages.media_path IS
5785
+ 'Onde os bytes ficaram no balde \`attachments\`, ou NULL: texto, linha anterior \xE0 013, ou upload que ainda n\xE3o aconteceu. Distingue-se pela combina\xE7\xE3o com \`message_type\` (013).';
5786
+
5787
+ -- Parcial: s\xF3 a m\xEDdia sem arquivo interessa, e \xE9 a minoria. O \xEDndice fica do
5788
+ -- tamanho do problema em vez do tamanho da tabela.
5789
+ CREATE INDEX IF NOT EXISTS plg_conversation_messages_midia_sem_arquivo
5790
+ ON public.plg_conversation_messages (tenant_id, at DESC)
5791
+ WHERE media_path IS NULL
5792
+ AND message_type IN ('image', 'video', 'audio', 'document', 'sticker');
5793
+ `;
5598
5794
  var MIGRATIONS = [
5599
5795
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
5600
5796
  { id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO },
@@ -5608,7 +5804,8 @@ var MIGRATIONS = [
5608
5804
  { id: "009_a_mensagem_inteira_atravessa_o_realtime", sql: MIGRATION_009_A_MENSAGEM_INTEIRA_ATRAVESSA_O_REALTIME },
5609
5805
  { id: "010_a_chegada_e_anunciada_por_conta", sql: MIGRATION_010_A_CHEGADA_E_ANUNCIADA_POR_CONTA },
5610
5806
  { 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 }
5807
+ { id: "012_a_rich_message_keeps_its_shape", sql: MIGRATION_012_A_RICH_MESSAGE_KEEPS_ITS_SHAPE },
5808
+ { id: "013_a_midia_fica_conosco", sql: MIGRATION_013_A_MIDIA_FICA_CONOSCO }
5612
5809
  ];
5613
5810
 
5614
5811
  // src/index.ts
@@ -5642,11 +5839,11 @@ function createConversationsPlugin(options) {
5642
5839
  contactEntityDef: options?.contactEntityDef
5643
5840
  };
5644
5841
  const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
5645
- const PageComponent = () => React11__default.createElement(ConversationsPage, { store: store2, config });
5842
+ const PageComponent = () => React12__default.createElement(ConversationsPage, { store: store2, config });
5646
5843
  PageComponent.displayName = "ConversationsPage";
5647
- const CampaignsPageComponent = () => React11__default.createElement(CampaignsView);
5844
+ const CampaignsPageComponent = () => React12__default.createElement(CampaignsView);
5648
5845
  CampaignsPageComponent.displayName = "ConversationsCampaignsPage";
5649
- const TemplatesPageComponent = () => React11__default.createElement(TemplatesView);
5846
+ const TemplatesPageComponent = () => React12__default.createElement(TemplatesView);
5650
5847
  TemplatesPageComponent.displayName = "ConversationsTemplatesPage";
5651
5848
  return {
5652
5849
  id: "conversations",
@@ -5722,7 +5919,7 @@ function createConversationsPlugin(options) {
5722
5919
  // quando ele serve para alguma coisa.
5723
5920
  zone: "shell.topbar.end",
5724
5921
  component: (() => {
5725
- const W = () => React11__default.createElement(InboxWatcher, {
5922
+ const W = () => React12__default.createElement(InboxWatcher, {
5726
5923
  label: options?.navLabel ?? "Conversations",
5727
5924
  section: options?.navSection ?? "main",
5728
5925
  position: options?.navPosition ?? 1