@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.
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.js +275 -78
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/MediaBubble.d.ts +8 -0
- package/dist/views/MediaBubble.d.ts.map +1 -0
- package/dist/views/MessageThread.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -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;AAoDD,wBAAgB,mCAAmC,CACjD,MAAM,CAAC,EAAE,2BAA2B,GACnC,qBAAqB,CAuRvB"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
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] =
|
|
215
|
-
const [templates, setTemplates] =
|
|
216
|
-
const [providers, setProviders] =
|
|
217
|
-
const [running, setRunning] =
|
|
218
|
-
const stop =
|
|
219
|
-
const load =
|
|
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
|
-
|
|
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] =
|
|
489
|
-
const podeTocar =
|
|
490
|
-
|
|
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 =
|
|
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
|
-
|
|
514
|
+
React12.useEffect(() => {
|
|
515
515
|
void contar();
|
|
516
516
|
}, [contar]);
|
|
517
|
-
|
|
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] =
|
|
553
|
-
const [theirs, setTheirs] =
|
|
554
|
-
const [providers, setProviders] =
|
|
555
|
-
const [busy, setBusy] =
|
|
556
|
-
const [draft, setDraft] =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
761
|
-
var ConfigContext =
|
|
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 =
|
|
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
|
|
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] =
|
|
792
|
+
const [matches, setMatches] = React12__default.useState(
|
|
793
793
|
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
794
794
|
);
|
|
795
|
-
|
|
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 =
|
|
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] =
|
|
1020
|
-
const [providers, setProviders] =
|
|
1021
|
-
|
|
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
|
-
|
|
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 =
|
|
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] =
|
|
1129
|
-
const [detail, setDetail] =
|
|
1130
|
-
const [loading, setLoading] =
|
|
1131
|
-
const [err, setErr] =
|
|
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] =
|
|
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] =
|
|
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 =
|
|
1271
|
-
|
|
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
|
-
|
|
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 =
|
|
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] =
|
|
1416
|
-
|
|
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] =
|
|
1584
|
-
const [contact, setContact] =
|
|
1585
|
-
const [typedHandle, setTypedHandle] =
|
|
1586
|
-
const [creatingContact, setCreatingContact] =
|
|
1587
|
-
const [firstMessage, setFirstMessage] =
|
|
1588
|
-
const [submitting, setSubmitting] =
|
|
1589
|
-
const [pickerKey, setPickerKey] =
|
|
1590
|
-
|
|
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 =
|
|
1838
|
+
const abertoRef = React12.useRef(selectedId);
|
|
1710
1839
|
abertoRef.current = selectedId;
|
|
1711
|
-
|
|
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] =
|
|
1727
|
-
const [newOpen, setNewOpen] =
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
2891
|
-
const [draft, setDraft] =
|
|
2892
|
-
const [busy, setBusy] =
|
|
2893
|
-
const [numbers, setNumbers] =
|
|
2894
|
-
const [qr, setQr] =
|
|
2895
|
-
const load =
|
|
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
|
-
|
|
3032
|
+
React12__default.useEffect(() => {
|
|
2903
3033
|
void load();
|
|
2904
3034
|
}, [load]);
|
|
2905
|
-
const byId =
|
|
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] =
|
|
3141
|
-
const [failed, setFailed] =
|
|
3142
|
-
|
|
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 = () =>
|
|
5842
|
+
const PageComponent = () => React12__default.createElement(ConversationsPage, { store: store2, config });
|
|
5646
5843
|
PageComponent.displayName = "ConversationsPage";
|
|
5647
|
-
const CampaignsPageComponent = () =>
|
|
5844
|
+
const CampaignsPageComponent = () => React12__default.createElement(CampaignsView);
|
|
5648
5845
|
CampaignsPageComponent.displayName = "ConversationsCampaignsPage";
|
|
5649
|
-
const TemplatesPageComponent = () =>
|
|
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 = () =>
|
|
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
|