@fayz-ai/plugin-conversations 0.13.1 → 0.14.1

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.
@@ -0,0 +1,11 @@
1
+ export declare function InboxWatcher({ label, route, section, position }: {
2
+ /** O rótulo da entrada de menu que recebe a conta. */
3
+ label: string;
4
+ route?: string;
5
+ /** Onde a entrada original vive. A contribuição tem de cair no MESMO lugar,
6
+ * senão ela não substitui a entrada — acrescenta uma segunda, com a conta,
7
+ * ao lado da que já lá está. */
8
+ section?: string;
9
+ position?: number;
10
+ }): null;
11
+ //# sourceMappingURL=InboxWatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InboxWatcher.d.ts","sourceRoot":"","sources":["../../src/components/InboxWatcher.tsx"],"names":[],"mappings":"AAiEA,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,QA8DA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAEvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGlD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAuBzD,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IAEpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oFAAoF;IACpF,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AA2BD,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA4P9F;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AACxG,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,+BAA+B,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAC3F,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,GACjC,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,qBAAqB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,oCAAoC,CAAA;AAM3C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC1F,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvH,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAEvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGlD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAuBzD,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IAEpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oFAAoF;IACpF,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AA2BD,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA+Q9F;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AACxG,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,+BAA+B,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAC3F,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,GACjC,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,qBAAqB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,oCAAoC,CAAA;AAM3C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC1F,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvH,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA"}
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import * as React9 from 'react';
2
- import React9__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
1
+ import * as React10 from 'react';
2
+ import React10__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
3
  import { Loader2, MessageSquare, AlertTriangle, Pause, Play, Send, RefreshCw, Plus, Clock, CheckCircle2, Check, ShieldCheck, ShieldAlert, QrCode, Zap, ExternalLink, SquarePen, Globe, Mail, Instagram, Phone, Search, Inbox, ChevronLeft, Archive, PanelRight, X, User, MapPin, Tag, StickyNote, Link2, MoreVertical, CheckCheck, Snowflake, Sun, Flame, Building2 } from 'lucide-react';
4
4
  import { toast, Button, Input, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, 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';
7
+ import { SettingsGroup, PluginSettingsPanel, useNavContribution, PermissionGate, useLimitGuard, RightRailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
7
8
  import { useStore } from 'zustand';
8
- import { SettingsGroup, PluginSettingsPanel, PermissionGate, useLimitGuard, RightRailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
9
9
  import { createStore } from 'zustand/vanilla';
10
10
 
11
11
  // src/index.ts
@@ -211,12 +211,12 @@ function Bar({ c }) {
211
211
  ] });
212
212
  }
213
213
  function CampaignsView() {
214
- const [rows, setRows] = React9__default.useState(null);
215
- const [templates, setTemplates] = React9__default.useState([]);
216
- const [providers, setProviders] = React9__default.useState([]);
217
- const [running, setRunning] = React9__default.useState(null);
218
- const stop = React9__default.useRef(false);
219
- const load = React9__default.useCallback(async () => {
214
+ const [rows, setRows] = React10__default.useState(null);
215
+ const [templates, setTemplates] = React10__default.useState([]);
216
+ const [providers, setProviders] = React10__default.useState([]);
217
+ const [running, setRunning] = React10__default.useState(null);
218
+ const stop = React10__default.useRef(false);
219
+ const load = React10__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
- React9__default.useEffect(() => {
234
+ React10__default.useEffect(() => {
235
235
  void load();
236
236
  }, [load]);
237
237
  const active = providers.find((p) => p.isActive);
@@ -376,6 +376,101 @@ function CampaignsView() {
376
376
  ] })
377
377
  ] });
378
378
  }
379
+
380
+ // src/data/tables.ts
381
+ var T = {
382
+ conversations: "plg_conversations",
383
+ messages: "plg_conversation_messages",
384
+ channels: "plg_conversations_channels"
385
+ };
386
+
387
+ // src/types.ts
388
+ var CHANNEL_LABELS = {
389
+ sms: "SMS",
390
+ whatsapp: "WhatsApp",
391
+ instagram: "Instagram",
392
+ email: "Email",
393
+ webchat: "Web Chat"
394
+ };
395
+ function isWaitingOnUs(conversation) {
396
+ if (conversation.status !== "open") return false;
397
+ if (conversation.lastMessageDirection) return conversation.lastMessageDirection === "inbound";
398
+ return conversation.unreadCount > 0;
399
+ }
400
+
401
+ // src/components/InboxWatcher.tsx
402
+ function tocar() {
403
+ try {
404
+ const Ctx = window.AudioContext ?? window.webkitAudioContext;
405
+ if (!Ctx) return;
406
+ const ctx = new Ctx();
407
+ const agora = ctx.currentTime;
408
+ for (const [freq, atraso] of [[880, 0], [1320, 0.09]]) {
409
+ const osc = ctx.createOscillator();
410
+ const vol = ctx.createGain();
411
+ osc.type = "sine";
412
+ osc.frequency.value = freq;
413
+ vol.gain.setValueAtTime(1e-4, agora + atraso);
414
+ vol.gain.exponentialRampToValueAtTime(0.18, agora + atraso + 0.02);
415
+ vol.gain.exponentialRampToValueAtTime(1e-4, agora + atraso + 0.28);
416
+ osc.connect(vol);
417
+ vol.connect(ctx.destination);
418
+ osc.start(agora + atraso);
419
+ osc.stop(agora + atraso + 0.3);
420
+ }
421
+ setTimeout(() => void ctx.close().catch(() => {
422
+ }), 900);
423
+ } catch {
424
+ }
425
+ }
426
+ function InboxWatcher({ label, route = "/conversations", section = "main", position = 1 }) {
427
+ const [naoLidas, setNaoLidas] = React10.useState(0);
428
+ const podeTocar = React10.useRef(false);
429
+ React10.useEffect(() => {
430
+ const liberar = () => {
431
+ podeTocar.current = true;
432
+ };
433
+ for (const ev of ["pointerdown", "keydown", "touchstart"]) {
434
+ window.addEventListener(ev, liberar, { once: true, passive: true });
435
+ }
436
+ return () => {
437
+ for (const ev of ["pointerdown", "keydown", "touchstart"]) window.removeEventListener(ev, liberar);
438
+ };
439
+ }, []);
440
+ const contar = React10.useCallback(async () => {
441
+ const supabase = getSupabaseClientOptional();
442
+ if (!supabase) return;
443
+ const { data } = await supabase.from(T.conversations).select("id, status, last_message_direction, unread_count").eq("status", "open");
444
+ if (!data) return;
445
+ const n = data.filter((r) => isWaitingOnUs({
446
+ status: r.status,
447
+ lastMessageDirection: r.last_message_direction ?? void 0,
448
+ unreadCount: r.unread_count ?? 0
449
+ })).length;
450
+ setNaoLidas(n);
451
+ }, []);
452
+ React10.useEffect(() => {
453
+ void contar();
454
+ }, [contar]);
455
+ React10.useEffect(() => {
456
+ const supabase = getSupabaseClientOptional();
457
+ if (!supabase?.channel) return;
458
+ const canal = supabase.channel("conversas:entrada").on(
459
+ "postgres_changes",
460
+ { event: "INSERT", schema: "public", table: T.messages },
461
+ (payload) => {
462
+ if (payload?.new?.direction !== "inbound") return;
463
+ void contar();
464
+ if (podeTocar.current) tocar();
465
+ }
466
+ ).subscribe();
467
+ return () => {
468
+ void supabase.removeChannel?.(canal);
469
+ };
470
+ }, [contar]);
471
+ useNavContribution("conversations:unread", naoLidas > 0 ? [{ id: "conversations", label, route, badge: naoLidas, badgeTone: "destructive", section, position }] : []);
472
+ return null;
473
+ }
379
474
  var STATUS_UI = {
380
475
  approved: { icon: CheckCircle2, cls: "text-success", label: "aprovado" },
381
476
  submitted: { icon: Clock, cls: "text-warning", label: "em an\xE1lise" },
@@ -390,14 +485,14 @@ function slots(body) {
390
485
  return found.size;
391
486
  }
392
487
  function TemplatesView() {
393
- const [mine, setMine] = React9__default.useState(null);
394
- const [theirs, setTheirs] = React9__default.useState([]);
395
- const [providers, setProviders] = React9__default.useState([]);
396
- const [busy, setBusy] = React9__default.useState(null);
397
- const [draft, setDraft] = React9__default.useState({ name: "", body: "", variables: "" });
488
+ const [mine, setMine] = React10__default.useState(null);
489
+ const [theirs, setTheirs] = React10__default.useState([]);
490
+ const [providers, setProviders] = React10__default.useState([]);
491
+ const [busy, setBusy] = React10__default.useState(null);
492
+ const [draft, setDraft] = React10__default.useState({ name: "", body: "", variables: "" });
398
493
  const active = providers.find((p) => p.isActive);
399
494
  const supportsTemplates = active?.capabilities?.templates !== false;
400
- const load = React9__default.useCallback(async () => {
495
+ const load = React10__default.useCallback(async () => {
401
496
  try {
402
497
  const [m, p] = await Promise.all([campaignsApi.templates(), providersApi.list()]);
403
498
  setMine(m);
@@ -407,7 +502,7 @@ function TemplatesView() {
407
502
  setMine([]);
408
503
  }
409
504
  }, []);
410
- React9__default.useEffect(() => {
505
+ React10__default.useEffect(() => {
411
506
  void load();
412
507
  }, [load]);
413
508
  const sync = async () => {
@@ -598,8 +693,8 @@ function TemplatesView() {
598
693
  var DEFAULT_CONVERSATIONS_CONFIG = {
599
694
  contactKind: "contact"
600
695
  };
601
- var StoreContext = React9__default.createContext(null);
602
- var ConfigContext = React9__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
696
+ var StoreContext = React10__default.createContext(null);
697
+ var ConfigContext = React10__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
603
698
  function ConversationsContextProvider({
604
699
  store: store2,
605
700
  config = DEFAULT_CONVERSATIONS_CONFIG,
@@ -608,29 +703,13 @@ function ConversationsContextProvider({
608
703
  return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store2, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
609
704
  }
610
705
  function useConversationsStore(selector) {
611
- const store2 = React9__default.useContext(StoreContext);
706
+ const store2 = React10__default.useContext(StoreContext);
612
707
  if (!store2) throw new Error("useConversationsStore must be used within ConversationsPage");
613
708
  return useStore(store2, selector);
614
709
  }
615
710
  function useConversationsConfig() {
616
- return React9__default.useContext(ConfigContext);
617
- }
618
-
619
- // src/types.ts
620
- var CHANNEL_LABELS = {
621
- sms: "SMS",
622
- whatsapp: "WhatsApp",
623
- instagram: "Instagram",
624
- email: "Email",
625
- webchat: "Web Chat"
626
- };
627
- function isWaitingOnUs(conversation) {
628
- if (conversation.status !== "open") return false;
629
- if (conversation.lastMessageDirection) return conversation.lastMessageDirection === "inbound";
630
- return conversation.unreadCount > 0;
711
+ return React10__default.useContext(ConfigContext);
631
712
  }
632
-
633
- // src/lib/channel.ts
634
713
  var CHANNEL_ICON = {
635
714
  sms: Phone,
636
715
  whatsapp: MessageSquare,
@@ -646,10 +725,10 @@ var CHANNEL_ACCENT = {
646
725
  webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
647
726
  };
648
727
  function useMediaQuery(query) {
649
- const [matches, setMatches] = React9__default.useState(
728
+ const [matches, setMatches] = React10__default.useState(
650
729
  () => typeof window !== "undefined" && window.matchMedia(query).matches
651
730
  );
652
- React9__default.useEffect(() => {
731
+ React10__default.useEffect(() => {
653
732
  if (typeof window === "undefined") return;
654
733
  const mql = window.matchMedia(query);
655
734
  const onChange = () => setMatches(mql.matches);
@@ -780,7 +859,7 @@ function ConversationList({ className }) {
780
859
  ] }),
781
860
  conversations.map((c) => {
782
861
  const active = c.id === selectedId;
783
- const unread = c.unreadCount > 0;
862
+ const unread = isWaitingOnUs(c);
784
863
  return /* @__PURE__ */ jsxs(
785
864
  "button",
786
865
  {
@@ -813,7 +892,7 @@ function ConversationList({ className }) {
813
892
  ] }),
814
893
  /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
815
894
  /* @__PURE__ */ jsx("span", { className: cn("truncate text-xs", unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
816
- unread && /* @__PURE__ */ jsx("span", { className: "flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-primary px-1.5 text-[10px] font-semibold text-primary-foreground", children: c.unreadCount })
895
+ unread && /* @__PURE__ */ jsx("span", { className: "flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-destructive px-1.5 text-[10px] font-semibold text-destructive-foreground", children: c.unreadCount > 0 ? c.unreadCount : "\xB7" })
817
896
  ] })
818
897
  ] })
819
898
  ]
@@ -825,9 +904,9 @@ function ConversationList({ className }) {
825
904
  ] });
826
905
  }
827
906
  function useSendWindow(conversationId) {
828
- const [row, setRow] = React9__default.useState(null);
829
- const [providers, setProviders] = React9__default.useState(null);
830
- React9__default.useEffect(() => {
907
+ const [row, setRow] = React10__default.useState(null);
908
+ const [providers, setProviders] = React10__default.useState(null);
909
+ React10__default.useEffect(() => {
831
910
  let alive = true;
832
911
  providersApi.list().then((p) => {
833
912
  if (alive) setProviders(p);
@@ -838,7 +917,7 @@ function useSendWindow(conversationId) {
838
917
  alive = false;
839
918
  };
840
919
  }, []);
841
- React9__default.useEffect(() => {
920
+ React10__default.useEffect(() => {
842
921
  let alive = true;
843
922
  if (!conversationId) {
844
923
  setRow(null);
@@ -859,7 +938,7 @@ function useSendWindow(conversationId) {
859
938
  };
860
939
  }, [conversationId]);
861
940
  const provider = (providers ?? []).find((p) => p.isActive);
862
- const mode = React9__default.useMemo(() => {
941
+ const mode = React10__default.useMemo(() => {
863
942
  if (!provider) return { kind: "blocked", reason: "no_provider" };
864
943
  if (!provider.capabilities?.requires_template) {
865
944
  return { kind: "free", until: null, minutesLeft: null };
@@ -934,10 +1013,10 @@ function DeliveryMark({ m }) {
934
1013
  ] });
935
1014
  }
936
1015
  function MessageMenu({ m }) {
937
- const [open, setOpen] = React9__default.useState(false);
938
- const [detail, setDetail] = React9__default.useState(null);
939
- const [loading, setLoading] = React9__default.useState(false);
940
- const [err, setErr] = React9__default.useState(null);
1016
+ const [open, setOpen] = React10__default.useState(false);
1017
+ const [detail, setDetail] = React10__default.useState(null);
1018
+ const [loading, setLoading] = React10__default.useState(false);
1019
+ const [err, setErr] = React10__default.useState(null);
941
1020
  async function load() {
942
1021
  setOpen(true);
943
1022
  if (!m.providerMessageId) return;
@@ -1028,14 +1107,14 @@ function buildRows(messages) {
1028
1107
  function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
1029
1108
  const t = useTranslation();
1030
1109
  const { messages, sending, send, setStatus, refreshMessages } = useConversationsStore((s) => s);
1031
- const [draft, setDraft] = React9__default.useState("");
1110
+ const [draft, setDraft] = React10__default.useState("");
1032
1111
  const win = useSendWindow(selected.id);
1033
1112
  const canType = win.mode.kind === "free";
1034
- const threadRef = React9__default.useRef(null);
1035
- React9__default.useEffect(() => {
1113
+ const threadRef = React10__default.useRef(null);
1114
+ React10__default.useEffect(() => {
1036
1115
  threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
1037
1116
  }, [messages.length, selected.id]);
1038
- React9__default.useEffect(() => {
1117
+ React10__default.useEffect(() => {
1039
1118
  if (!selected.id) return;
1040
1119
  let timer = null;
1041
1120
  const start = () => {
@@ -1064,7 +1143,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
1064
1143
  await send(body);
1065
1144
  }
1066
1145
  const accent = CHANNEL_ACCENT[selected.channel];
1067
- const rows = React9__default.useMemo(() => buildRows(messages), [messages]);
1146
+ const rows = React10__default.useMemo(() => buildRows(messages), [messages]);
1068
1147
  return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
1069
1148
  /* @__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: [
1070
1149
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
@@ -1176,8 +1255,8 @@ var TEMP = {
1176
1255
  cold: { icon: Snowflake, cls: "bg-info-soft text-info-soft-foreground", label: "Frio" }
1177
1256
  };
1178
1257
  function LeadCard({ personId, handle }) {
1179
- const [lead, setLead] = React9__default.useState(void 0);
1180
- React9__default.useEffect(() => {
1258
+ const [lead, setLead] = React10__default.useState(void 0);
1259
+ React10__default.useEffect(() => {
1181
1260
  let alive = true;
1182
1261
  const sb = getSupabaseClientOptional();
1183
1262
  const tenantId = getActiveTenantId();
@@ -1340,14 +1419,14 @@ function NewConversationPanel({
1340
1419
  const create = useConversationsStore((s) => s.create);
1341
1420
  const config = useConversationsConfig();
1342
1421
  const guardConversations = useLimitGuard("conversations_month");
1343
- const [channel, setChannel] = React9__default.useState("whatsapp");
1344
- const [contact, setContact] = React9__default.useState(null);
1345
- const [typedHandle, setTypedHandle] = React9__default.useState("");
1346
- const [creatingContact, setCreatingContact] = React9__default.useState(false);
1347
- const [firstMessage, setFirstMessage] = React9__default.useState("");
1348
- const [submitting, setSubmitting] = React9__default.useState(false);
1349
- const [pickerKey, setPickerKey] = React9__default.useState(0);
1350
- React9__default.useEffect(() => {
1422
+ const [channel, setChannel] = React10__default.useState("whatsapp");
1423
+ const [contact, setContact] = React10__default.useState(null);
1424
+ const [typedHandle, setTypedHandle] = React10__default.useState("");
1425
+ const [creatingContact, setCreatingContact] = React10__default.useState(false);
1426
+ const [firstMessage, setFirstMessage] = React10__default.useState("");
1427
+ const [submitting, setSubmitting] = React10__default.useState(false);
1428
+ const [pickerKey, setPickerKey] = React10__default.useState(0);
1429
+ React10__default.useEffect(() => {
1351
1430
  if (open) {
1352
1431
  setChannel("whatsapp");
1353
1432
  setContact(null);
@@ -1465,10 +1544,10 @@ function InboxView() {
1465
1544
  const t = useTranslation();
1466
1545
  const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
1467
1546
  const isWidePanel = useMediaQuery("(min-width: 1280px)");
1468
- const [panelOpen, setPanelOpen] = React9__default.useState(false);
1469
- const [newOpen, setNewOpen] = React9__default.useState(false);
1547
+ const [panelOpen, setPanelOpen] = React10__default.useState(false);
1548
+ const [newOpen, setNewOpen] = React10__default.useState(false);
1470
1549
  const selected = conversations.find((c) => c.id === selectedId) ?? null;
1471
- React9__default.useEffect(() => {
1550
+ React10__default.useEffect(() => {
1472
1551
  setPanelOpen(isWidePanel);
1473
1552
  }, [isWidePanel]);
1474
1553
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1541,7 +1620,7 @@ function requestedContact() {
1541
1620
  }
1542
1621
  }
1543
1622
  function ConversationsPage({ store: store2, config }) {
1544
- React9__default.useEffect(() => {
1623
+ React10__default.useEffect(() => {
1545
1624
  const wanted = requestedContact();
1546
1625
  if (!wanted) {
1547
1626
  void store2.getState().load();
@@ -1828,15 +1907,6 @@ function createMockConversationsProvider(config) {
1828
1907
  }
1829
1908
  };
1830
1909
  }
1831
-
1832
- // src/data/tables.ts
1833
- var T = {
1834
- conversations: "plg_conversations",
1835
- messages: "plg_conversation_messages",
1836
- channels: "plg_conversations_channels"
1837
- };
1838
-
1839
- // src/data/supabase.ts
1840
1910
  function mapConversation(r) {
1841
1911
  return {
1842
1912
  id: String(r.id),
@@ -2596,22 +2666,22 @@ function NumberRow({ n }) {
2596
2666
  ] });
2597
2667
  }
2598
2668
  function WhatsAppProviders() {
2599
- const [rows, setRows] = React9__default.useState(null);
2600
- const [draft, setDraft] = React9__default.useState({});
2601
- const [busy, setBusy] = React9__default.useState(null);
2602
- const [numbers, setNumbers] = React9__default.useState({});
2603
- const [qr, setQr] = React9__default.useState(null);
2604
- const load = React9__default.useCallback(async () => {
2669
+ const [rows, setRows] = React10__default.useState(null);
2670
+ const [draft, setDraft] = React10__default.useState({});
2671
+ const [busy, setBusy] = React10__default.useState(null);
2672
+ const [numbers, setNumbers] = React10__default.useState({});
2673
+ const [qr, setQr] = React10__default.useState(null);
2674
+ const load = React10__default.useCallback(async () => {
2605
2675
  try {
2606
2676
  setRows(await providersApi.list());
2607
2677
  } catch {
2608
2678
  setRows([]);
2609
2679
  }
2610
2680
  }, []);
2611
- React9__default.useEffect(() => {
2681
+ React10__default.useEffect(() => {
2612
2682
  void load();
2613
2683
  }, [load]);
2614
- const byId = React9__default.useMemo(
2684
+ const byId = React10__default.useMemo(
2615
2685
  () => Object.fromEntries((rows ?? []).map((r) => [r.provider, r])),
2616
2686
  [rows]
2617
2687
  );
@@ -2846,9 +2916,9 @@ function ChannelRow({ account }) {
2846
2916
  }
2847
2917
  function ConversationsGeneralSettings() {
2848
2918
  const t = useTranslation();
2849
- const [channels, setChannels] = React9.useState(null);
2850
- const [failed, setFailed] = React9.useState(false);
2851
- React9.useEffect(() => {
2919
+ const [channels, setChannels] = React10.useState(null);
2920
+ const [failed, setFailed] = React10.useState(false);
2921
+ React10.useEffect(() => {
2852
2922
  let cancelled = false;
2853
2923
  listMessagingChannels().then((rows) => {
2854
2924
  if (!cancelled) setChannels(rows);
@@ -4803,13 +4873,140 @@ BEGIN
4803
4873
  ON public.plg_crm_activities (tenant_id, lead_id, activity_type, completed_at DESC);
4804
4874
  END $$;
4805
4875
  `;
4876
+ var MIGRATION_006_A_CAIXA_AVISA_QUE_CHEGOU = `-- ---------------------------------------------------------------------------
4877
+ -- 006_a_caixa_avisa_que_chegou.sql
4878
+ --
4879
+ -- \u2500\u2500 o sil\xEAncio, e de onde ele vinha \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
4880
+ --
4881
+ -- A caixa recebia mensagens e ningu\xE9m era avisado. Sem som, sem contagem no
4882
+ -- menu, sem destaque na lista \u2014 e a suspeita natural \xE9 que falta a tela que
4883
+ -- avisa. N\xE3o faltava: o evento nunca sa\xEDa do banco.
4884
+ --
4885
+ -- \`plg_conversation_messages\` e \`plg_conversations\` n\xE3o estavam em
4886
+ -- \`supabase_realtime\`. A publica\xE7\xE3o existe (\`puballtables = false\`, ou seja,
4887
+ -- cada tabela entra nomeada) e nenhuma das duas fora nomeada. Qualquer cliente
4888
+ -- que assinasse \`postgres_changes\` sobre elas ficaria subscrito para sempre a
4889
+ -- um canal por onde nada passa \u2014 e subscrever com sucesso a um canal mudo \xE9 o
4890
+ -- modo de falhar mais convincente que h\xE1: o c\xF3digo parece certo, o console
4891
+ -- fica limpo, e nada acontece.
4892
+ --
4893
+ -- \u2500\u2500 as duas, e n\xE3o s\xF3 as mensagens \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
4894
+ --
4895
+ -- A mensagem \xE9 o que dispara o alerta; a CONVERSA \xE9 o que carrega o estado que
4896
+ -- a lista pinta (\`status\`, \`unread_count\`, \`last_message_direction\`). Publicar
4897
+ -- s\xF3 a primeira daria o som sem o badge parar de mentir depois de algu\xE9m ler.
4898
+ --
4899
+ -- \u2500\u2500 o RLS continua a valer \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
4900
+ --
4901
+ -- Publicar n\xE3o abre nada: o Realtime do Supabase aplica as pol\xEDticas da tabela
4902
+ -- por assinante, e quem n\xE3o pode ler a linha n\xE3o a recebe. O que a publica\xE7\xE3o
4903
+ -- decide \xE9 se o evento EXISTE, n\xE3o quem o v\xEA.
4904
+ --
4905
+ -- \u2500\u2500 idempotente, e silenciosa onde n\xE3o se aplica \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4906
+ --
4907
+ -- \`ALTER PUBLICATION ... ADD TABLE\` rebenta se a tabela j\xE1 l\xE1 est\xE1, e a
4908
+ -- publica\xE7\xE3o \`supabase_realtime\` s\xF3 existe num Postgres da Supabase. As duas
4909
+ -- condi\xE7\xF5es s\xE3o verificadas antes, porque esta migration tamb\xE9m roda no banco
4910
+ -- de teste, que n\xE3o tem nenhuma delas.
4911
+ -- ---------------------------------------------------------------------------
4912
+
4913
+ DO $$
4914
+ DECLARE
4915
+ t text;
4916
+ v_n integer := 0;
4917
+ BEGIN
4918
+ IF NOT EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'supabase_realtime') THEN
4919
+ RAISE NOTICE 'conversations/006: sem publica\xE7\xE3o supabase_realtime \u2014 nada a fazer';
4920
+ RETURN;
4921
+ END IF;
4922
+
4923
+ FOREACH t IN ARRAY ARRAY['plg_conversation_messages', 'plg_conversations'] LOOP
4924
+ CONTINUE WHEN to_regclass('public.' || t) IS NULL;
4925
+ CONTINUE WHEN EXISTS (
4926
+ SELECT 1 FROM pg_publication_tables
4927
+ WHERE pubname = 'supabase_realtime' AND schemaname = 'public' AND tablename = t);
4928
+ EXECUTE format('ALTER PUBLICATION supabase_realtime ADD TABLE public.%I', t);
4929
+ v_n := v_n + 1;
4930
+ END LOOP;
4931
+
4932
+ RAISE NOTICE 'conversations/006: % tabela(s) publicada(s) no realtime', v_n;
4933
+ END $$;
4934
+
4935
+ -- \`REPLICA IDENTITY FULL\` na conversa: sem ela o UPDATE chega com a chave
4936
+ -- prim\xE1ria e mais nada, e a tela n\xE3o consegue saber que \`unread_count\` mudou \u2014
4937
+ -- que \xE9 exatamente o UPDATE que interessa aqui. Na tabela de mensagens n\xE3o \xE9
4938
+ -- preciso: o que importa \xE9 o INSERT, e esse vem inteiro de qualquer forma.
4939
+ DO $$
4940
+ BEGIN
4941
+ IF to_regclass('public.plg_conversations') IS NOT NULL THEN
4942
+ ALTER TABLE public.plg_conversations REPLICA IDENTITY FULL;
4943
+ END IF;
4944
+ END $$;
4945
+
4946
+ -- \u2500\u2500 e a conta de n\xE3o lidas, que ningu\xE9m mantinha \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4947
+ --
4948
+ -- \`unread_count\` estava em ZERO nas oito conversas do cluster, incluindo as
4949
+ -- quatro cuja \xFAltima mensagem \xE9 de entrada. N\xE3o era um zero verdadeiro: n\xE3o h\xE1
4950
+ -- (n\xE3o havia) nada que a incremente. A coluna existe desde a baseline, a tela
4951
+ -- da lista decide o destaque por ela (\`unreadCount > 0\`) e a RPC de marcar como
4952
+ -- lida zera-a \u2014 s\xF3 faltava o lado que soma.
4953
+ --
4954
+ -- Por isso \`isWaitingOnUs\` recorre a \`lastMessageDirection\` quando a conta \xE9
4955
+ -- zero: foi escrita j\xE1 sabendo que a conta n\xE3o era de fiar. Esta migration
4956
+ -- torna-a de fiar, e a recorr\xEAncia volta a ser o que devia ser \u2014 uma rede, e
4957
+ -- n\xE3o o caminho normal.
4958
+ --
4959
+ -- S\xF3 o que CHEGA conta. Somar o que n\xF3s enviamos faria a caixa dizer que h\xE1
4960
+ -- doze por ler logo depois de algu\xE9m responder doze vezes.
4961
+ CREATE OR REPLACE FUNCTION public.plg_conversations_bump_unread() RETURNS trigger
4962
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
4963
+ AS $$
4964
+ BEGIN
4965
+ IF NEW.direction IS DISTINCT FROM 'inbound' THEN RETURN NULL; END IF;
4966
+ UPDATE public.plg_conversations
4967
+ SET unread_count = coalesce(unread_count, 0) + 1
4968
+ WHERE id = NEW.conversation_id;
4969
+ RETURN NULL;
4970
+ END $$;
4971
+
4972
+ REVOKE ALL ON FUNCTION public.plg_conversations_bump_unread() FROM PUBLIC;
4973
+ GRANT EXECUTE ON FUNCTION public.plg_conversations_bump_unread() TO authenticated, service_role;
4974
+
4975
+ COMMENT ON FUNCTION public.plg_conversations_bump_unread() IS
4976
+ 'Soma uma n\xE3o lida por mensagem que CHEGA. A coluna existia e era zerada ao ler, mas nada a incrementava (006).';
4977
+
4978
+ -- AFTER, como o irm\xE3o que carimba a dire\xE7\xE3o: falhar a contar n\xE3o pode recusar
4979
+ -- uma mensagem que o provedor j\xE1 entregou.
4980
+ DROP TRIGGER IF EXISTS plg_conversation_messages_bump_unread ON public.plg_conversation_messages;
4981
+ CREATE TRIGGER plg_conversation_messages_bump_unread
4982
+ AFTER INSERT ON public.plg_conversation_messages
4983
+ FOR EACH ROW EXECUTE FUNCTION public.plg_conversations_bump_unread();
4984
+
4985
+ -- O retroativo, e o que ele pode e n\xE3o pode saber.
4986
+ --
4987
+ -- N\xE3o h\xE1 coluna de "lido at\xE9 quando" nesta tabela \u2014 \`markRead\` zera a conta e
4988
+ -- mais nada. Ent\xE3o n\xE3o d\xE1 para reconstituir quantas mensagens ficaram por ler:
4989
+ -- o que d\xE1 para saber \xE9 que a conversa EST\xC1 \xE0 espera, porque a \xFAltima mensagem
4990
+ -- dela \xE9 de entrada.
4991
+ --
4992
+ -- Nesse caso a conta vira 1. Um n\xFAmero inventado seria pior que um n\xFAmero
4993
+ -- modesto: 1 diz "h\xE1 algo aqui", que \xE9 verdade e \xE9 o que faz a linha destacar;
4994
+ -- dizer "7" alegaria uma precis\xE3o que esta tabela nunca guardou. A partir de
4995
+ -- agora o gatilho conta de verdade.
4996
+ UPDATE public.plg_conversations
4997
+ SET unread_count = 1
4998
+ WHERE last_message_direction = 'inbound'
4999
+ AND status = 'open'
5000
+ AND coalesce(unread_count, 0) = 0;
5001
+ `;
4806
5002
  var MIGRATIONS = [
4807
5003
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
4808
5004
  { id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO },
4809
5005
  { id: "002_a_casa_escolhe_por_onde_o_whatsapp_sai", sql: MIGRATION_002_A_CASA_ESCOLHE_POR_ONDE_O_WHATSAPP_SAI },
4810
5006
  { id: "003_a_campanha_sai_no_ritmo_que_o_numero_aguenta", sql: MIGRATION_003_A_CAMPANHA_SAI_NO_RITMO_QUE_O_NUMERO_AGUENTA },
4811
5007
  { id: "004_a_tela_sabe_se_pode_falar_antes_de_deixar_escrever", sql: MIGRATION_004_A_TELA_SABE_SE_PODE_FALAR_ANTES_DE_DEIXAR_ESCREVER },
4812
- { id: "005_a_conversa_conta_ao_funil_que_houve_contato", sql: MIGRATION_005_A_CONVERSA_CONTA_AO_FUNIL_QUE_HOUVE_CONTATO }
5008
+ { id: "005_a_conversa_conta_ao_funil_que_houve_contato", sql: MIGRATION_005_A_CONVERSA_CONTA_AO_FUNIL_QUE_HOUVE_CONTATO },
5009
+ { id: "006_a_caixa_avisa_que_chegou", sql: MIGRATION_006_A_CAIXA_AVISA_QUE_CHEGOU }
4813
5010
  ];
4814
5011
 
4815
5012
  // src/index.ts
@@ -4843,11 +5040,11 @@ function createConversationsPlugin(options) {
4843
5040
  contactEntityDef: options?.contactEntityDef
4844
5041
  };
4845
5042
  const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
4846
- const PageComponent = () => React9__default.createElement(ConversationsPage, { store: store2, config });
5043
+ const PageComponent = () => React10__default.createElement(ConversationsPage, { store: store2, config });
4847
5044
  PageComponent.displayName = "ConversationsPage";
4848
- const CampaignsPageComponent = () => React9__default.createElement(CampaignsView);
5045
+ const CampaignsPageComponent = () => React10__default.createElement(CampaignsView);
4849
5046
  CampaignsPageComponent.displayName = "ConversationsCampaignsPage";
4850
- const TemplatesPageComponent = () => React9__default.createElement(TemplatesView);
5047
+ const TemplatesPageComponent = () => React10__default.createElement(TemplatesView);
4851
5048
  TemplatesPageComponent.displayName = "ConversationsTemplatesPage";
4852
5049
  return {
4853
5050
  id: "conversations",
@@ -4914,7 +5111,26 @@ function createConversationsPlugin(options) {
4914
5111
  permission: { feature: "conversations", action: "read" }
4915
5112
  }
4916
5113
  ],
4917
- widgets: [],
5114
+ widgets: [
5115
+ {
5116
+ id: "conversations-inbox-watcher",
5117
+ // `shell.topbar.end` é a única zona global que o AdminShell renderiza
5118
+ // FORA do switch de rota: navegar não a desmonta. Um vigia montado
5119
+ // dentro da tela de Conversas morreria ao sair dela — que é justamente
5120
+ // quando ele serve para alguma coisa.
5121
+ zone: "shell.topbar.end",
5122
+ component: (() => {
5123
+ const W = () => React10__default.createElement(InboxWatcher, {
5124
+ label: options?.navLabel ?? "Conversations",
5125
+ section: options?.navSection ?? "main",
5126
+ position: options?.navPosition ?? 1
5127
+ });
5128
+ W.displayName = "InboxWatcher";
5129
+ return W;
5130
+ })(),
5131
+ order: -20
5132
+ }
5133
+ ],
4918
5134
  dashboardWidgets,
4919
5135
  events: [
4920
5136
  { name: "conversations.message.received", description: "An inbound message arrived on any channel" },