@fayz-ai/plugin-conversations 0.17.1 → 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 +388 -78
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +45 -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 +1 -1
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 } 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,6 +1199,180 @@ 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
|
+
}
|
|
1330
|
+
function ContactCard({ contacts, outbound }) {
|
|
1331
|
+
const [copied, setCopied] = React12__default.useState(null);
|
|
1332
|
+
const copy = (value) => {
|
|
1333
|
+
void navigator.clipboard?.writeText(value);
|
|
1334
|
+
setCopied(value);
|
|
1335
|
+
window.setTimeout(() => setCopied((c) => c === value ? null : c), 1500);
|
|
1336
|
+
};
|
|
1337
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: contacts.map((c, i) => /* @__PURE__ */ jsxs(
|
|
1338
|
+
"div",
|
|
1339
|
+
{
|
|
1340
|
+
className: cn(
|
|
1341
|
+
"flex items-start gap-2.5 rounded-xl px-2.5 py-2",
|
|
1342
|
+
outbound ? "bg-white/15" : "bg-muted"
|
|
1343
|
+
),
|
|
1344
|
+
children: [
|
|
1345
|
+
/* @__PURE__ */ jsx(UserRound, { className: cn("mt-0.5 h-4 w-4 shrink-0", outbound ? "text-white/80" : "text-muted-foreground") }),
|
|
1346
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1347
|
+
/* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: c.name }),
|
|
1348
|
+
c.phones.map((p) => /* @__PURE__ */ jsxs(
|
|
1349
|
+
"button",
|
|
1350
|
+
{
|
|
1351
|
+
type: "button",
|
|
1352
|
+
onClick: () => copy(p.display),
|
|
1353
|
+
className: cn(
|
|
1354
|
+
"group/phone mt-0.5 flex items-center gap-1.5 text-left text-[13px] tabular-nums",
|
|
1355
|
+
outbound ? "text-white/90 hover:text-white" : "text-muted-foreground hover:text-foreground"
|
|
1356
|
+
),
|
|
1357
|
+
children: [
|
|
1358
|
+
p.display,
|
|
1359
|
+
copied === p.display ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3 opacity-0 transition-opacity group-hover/phone:opacity-100" })
|
|
1360
|
+
]
|
|
1361
|
+
},
|
|
1362
|
+
p.digits
|
|
1363
|
+
))
|
|
1364
|
+
] })
|
|
1365
|
+
]
|
|
1366
|
+
},
|
|
1367
|
+
`${c.name}-${i}`
|
|
1368
|
+
)) });
|
|
1369
|
+
}
|
|
1370
|
+
function MessageBody({ m, outbound }) {
|
|
1371
|
+
const contacts = m.messageType === "contact" ? m.payload?.contacts ?? [] : [];
|
|
1372
|
+
if (contacts.length > 0) return /* @__PURE__ */ jsx(ContactCard, { contacts, outbound });
|
|
1373
|
+
if (temArquivo(m)) return /* @__PURE__ */ jsx(MediaBubble, { m, outbound });
|
|
1374
|
+
return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: m.body });
|
|
1375
|
+
}
|
|
1202
1376
|
function buildRows(messages) {
|
|
1203
1377
|
const rows = [];
|
|
1204
1378
|
let lastDay = "";
|
|
@@ -1219,14 +1393,14 @@ function buildRows(messages) {
|
|
|
1219
1393
|
function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
|
|
1220
1394
|
const t = useTranslation();
|
|
1221
1395
|
const { messages, sending, send, setStatus, refreshMessages } = useConversationsStore((s) => s);
|
|
1222
|
-
const [draft, setDraft] =
|
|
1396
|
+
const [draft, setDraft] = React12__default.useState("");
|
|
1223
1397
|
const win = useSendWindow(selected.id);
|
|
1224
1398
|
const canType = win.mode.kind === "free";
|
|
1225
|
-
const threadRef =
|
|
1226
|
-
|
|
1399
|
+
const threadRef = React12__default.useRef(null);
|
|
1400
|
+
React12__default.useEffect(() => {
|
|
1227
1401
|
threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
|
|
1228
1402
|
}, [messages.length, selected.id]);
|
|
1229
|
-
|
|
1403
|
+
React12__default.useEffect(() => {
|
|
1230
1404
|
if (!selected.id) return;
|
|
1231
1405
|
let timer = null;
|
|
1232
1406
|
const start = () => {
|
|
@@ -1255,7 +1429,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
|
|
|
1255
1429
|
await send(body);
|
|
1256
1430
|
}
|
|
1257
1431
|
const accent = CHANNEL_ACCENT[selected.channel];
|
|
1258
|
-
const rows =
|
|
1432
|
+
const rows = React12__default.useMemo(() => buildRows(messages), [messages]);
|
|
1259
1433
|
return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
|
|
1260
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: [
|
|
1261
1435
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
|
|
@@ -1317,7 +1491,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
|
|
|
1317
1491
|
),
|
|
1318
1492
|
style: outbound ? { backgroundColor: failed ? "hsl(var(--destructive))" : accent.color } : void 0,
|
|
1319
1493
|
children: [
|
|
1320
|
-
/* @__PURE__ */ jsx(
|
|
1494
|
+
/* @__PURE__ */ jsx(MessageBody, { m, outbound }),
|
|
1321
1495
|
/* @__PURE__ */ jsxs("div", { className: cn("mt-0.5 flex items-center justify-end gap-1 text-[10px]", outbound ? "text-white/70" : "text-muted-foreground"), children: [
|
|
1322
1496
|
new Date(m.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
1323
1497
|
/* @__PURE__ */ jsx(DeliveryMark, { m })
|
|
@@ -1367,8 +1541,8 @@ var TEMP = {
|
|
|
1367
1541
|
cold: { icon: Snowflake, cls: "bg-info-soft text-info-soft-foreground", label: "Frio" }
|
|
1368
1542
|
};
|
|
1369
1543
|
function LeadCard({ personId, handle }) {
|
|
1370
|
-
const [lead, setLead] =
|
|
1371
|
-
|
|
1544
|
+
const [lead, setLead] = React12__default.useState(void 0);
|
|
1545
|
+
React12__default.useEffect(() => {
|
|
1372
1546
|
let alive = true;
|
|
1373
1547
|
const sb = getSupabaseClientOptional();
|
|
1374
1548
|
const tenantId = getActiveTenantId();
|
|
@@ -1535,14 +1709,14 @@ function NewConversationPanel({
|
|
|
1535
1709
|
const create = useConversationsStore((s) => s.create);
|
|
1536
1710
|
const config = useConversationsConfig();
|
|
1537
1711
|
const guardConversations = useLimitGuard("conversations_month");
|
|
1538
|
-
const [channel, setChannel] =
|
|
1539
|
-
const [contact, setContact] =
|
|
1540
|
-
const [typedHandle, setTypedHandle] =
|
|
1541
|
-
const [creatingContact, setCreatingContact] =
|
|
1542
|
-
const [firstMessage, setFirstMessage] =
|
|
1543
|
-
const [submitting, setSubmitting] =
|
|
1544
|
-
const [pickerKey, setPickerKey] =
|
|
1545
|
-
|
|
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(() => {
|
|
1546
1720
|
if (open) {
|
|
1547
1721
|
setChannel("whatsapp");
|
|
1548
1722
|
setContact(null);
|
|
@@ -1661,9 +1835,9 @@ function useCaixaAoVivo() {
|
|
|
1661
1835
|
const load = useConversationsStore((s) => s.load);
|
|
1662
1836
|
const refreshMessages = useConversationsStore((s) => s.refreshMessages);
|
|
1663
1837
|
const selectedId = useConversationsStore((s) => s.selectedId);
|
|
1664
|
-
const abertoRef =
|
|
1838
|
+
const abertoRef = React12.useRef(selectedId);
|
|
1665
1839
|
abertoRef.current = selectedId;
|
|
1666
|
-
|
|
1840
|
+
React12.useEffect(() => {
|
|
1667
1841
|
if (!tenantId) return;
|
|
1668
1842
|
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
1669
1843
|
void load();
|
|
@@ -1678,10 +1852,10 @@ function InboxView() {
|
|
|
1678
1852
|
const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
|
|
1679
1853
|
useCaixaAoVivo();
|
|
1680
1854
|
const isWidePanel = useMediaQuery("(min-width: 1280px)");
|
|
1681
|
-
const [panelOpen, setPanelOpen] =
|
|
1682
|
-
const [newOpen, setNewOpen] =
|
|
1855
|
+
const [panelOpen, setPanelOpen] = React12__default.useState(false);
|
|
1856
|
+
const [newOpen, setNewOpen] = React12__default.useState(false);
|
|
1683
1857
|
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
1684
|
-
|
|
1858
|
+
React12__default.useEffect(() => {
|
|
1685
1859
|
setPanelOpen(isWidePanel);
|
|
1686
1860
|
}, [isWidePanel]);
|
|
1687
1861
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1754,7 +1928,7 @@ function requestedContact() {
|
|
|
1754
1928
|
}
|
|
1755
1929
|
}
|
|
1756
1930
|
function ConversationsPage({ store: store2, config }) {
|
|
1757
|
-
|
|
1931
|
+
React12__default.useEffect(() => {
|
|
1758
1932
|
const wanted = requestedContact();
|
|
1759
1933
|
if (!wanted) {
|
|
1760
1934
|
void store2.getState().load();
|
|
@@ -2078,7 +2252,13 @@ function mapMessage(r) {
|
|
|
2078
2252
|
at: r.at ?? "",
|
|
2079
2253
|
author: r.author ?? "",
|
|
2080
2254
|
deliveryStatus: r.delivery_status ?? null,
|
|
2081
|
-
providerMessageId: r.provider_message_id ?? null
|
|
2255
|
+
providerMessageId: r.provider_message_id ?? null,
|
|
2256
|
+
// NULL stays null rather than defaulting to 'text'. A row written before
|
|
2257
|
+
// migration 012 genuinely does not know what it was, and the rows that most
|
|
2258
|
+
// need saying so are the empty ones that a 'text' default would call fine.
|
|
2259
|
+
messageType: r.message_type ?? null,
|
|
2260
|
+
payload: r.payload ?? null,
|
|
2261
|
+
mediaPath: r.media_path ?? null
|
|
2082
2262
|
};
|
|
2083
2263
|
}
|
|
2084
2264
|
function createSupabaseConversationsProvider(config) {
|
|
@@ -2837,22 +3017,22 @@ function NumberRow({ n }) {
|
|
|
2837
3017
|
] });
|
|
2838
3018
|
}
|
|
2839
3019
|
function WhatsAppProviders() {
|
|
2840
|
-
const [rows, setRows] =
|
|
2841
|
-
const [draft, setDraft] =
|
|
2842
|
-
const [busy, setBusy] =
|
|
2843
|
-
const [numbers, setNumbers] =
|
|
2844
|
-
const [qr, setQr] =
|
|
2845
|
-
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 () => {
|
|
2846
3026
|
try {
|
|
2847
3027
|
setRows(await providersApi.list());
|
|
2848
3028
|
} catch {
|
|
2849
3029
|
setRows([]);
|
|
2850
3030
|
}
|
|
2851
3031
|
}, []);
|
|
2852
|
-
|
|
3032
|
+
React12__default.useEffect(() => {
|
|
2853
3033
|
void load();
|
|
2854
3034
|
}, [load]);
|
|
2855
|
-
const byId =
|
|
3035
|
+
const byId = React12__default.useMemo(
|
|
2856
3036
|
() => Object.fromEntries((rows ?? []).map((r) => [r.provider, r])),
|
|
2857
3037
|
[rows]
|
|
2858
3038
|
);
|
|
@@ -3087,9 +3267,9 @@ function ChannelRow({ account }) {
|
|
|
3087
3267
|
}
|
|
3088
3268
|
function ConversationsGeneralSettings() {
|
|
3089
3269
|
const t = useTranslation();
|
|
3090
|
-
const [channels, setChannels] =
|
|
3091
|
-
const [failed, setFailed] =
|
|
3092
|
-
|
|
3270
|
+
const [channels, setChannels] = React12.useState(null);
|
|
3271
|
+
const [failed, setFailed] = React12.useState(false);
|
|
3272
|
+
React12.useEffect(() => {
|
|
3093
3273
|
let cancelled = false;
|
|
3094
3274
|
listMessagingChannels().then((rows) => {
|
|
3095
3275
|
if (!cancelled) setChannels(rows);
|
|
@@ -5483,6 +5663,134 @@ END $$;
|
|
|
5483
5663
|
COMMENT ON FUNCTION public.plg_conversations_anunciar_chegada() IS
|
|
5484
5664
|
'Toca a campainha no t\xF3pico \`conversas:<tenant>\` \u2014 s\xF3 o id da conversa. O conte\xFAdo \xE9 lido da tabela, com RLS (011).';
|
|
5485
5665
|
`;
|
|
5666
|
+
var MIGRATION_012_A_RICH_MESSAGE_KEEPS_ITS_SHAPE = `-- ---------------------------------------------------------------------------
|
|
5667
|
+
-- 012_a_rich_message_keeps_its_shape.sql
|
|
5668
|
+
--
|
|
5669
|
+
-- \u2500\u2500 what was lost, and how \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5670
|
+
--
|
|
5671
|
+
-- \`plg_conversation_messages\` has exactly one content column: \`body text\`. A
|
|
5672
|
+
-- WhatsApp message that is not text therefore arrives flattened to whatever
|
|
5673
|
+
-- string the ingest could make of it \u2014 and when the ingest could make none, to
|
|
5674
|
+
-- the empty string, which renders as an empty bubble.
|
|
5675
|
+
--
|
|
5676
|
+
-- On 2026-09-22 a lead sent his finance director's contact card. It stored as
|
|
5677
|
+
-- \`body = ''\`. He then sent a screenshot of the same card; that stored as
|
|
5678
|
+
-- \`body = ''\` too. The fact the conversation existed to obtain \u2014 a name and a
|
|
5679
|
+
-- phone number \u2014 was in the payload both times and in the row neither time.
|
|
5680
|
+
--
|
|
5681
|
+
-- Recovering it meant going back to the Evolution instance and reading the
|
|
5682
|
+
-- provider's own copy. That worked, but it is not a recovery path: Evolution
|
|
5683
|
+
-- prunes, the instance is local-only, and a row that needs a second system to
|
|
5684
|
+
-- be legible is not a record.
|
|
5685
|
+
--
|
|
5686
|
+
-- \u2500\u2500 the two columns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5687
|
+
--
|
|
5688
|
+
-- \`message_type\` names what the provider sent \u2014 text, contact, image, audio,
|
|
5689
|
+
-- document, sticker, location, reaction, unknown. The inbox needs it to choose
|
|
5690
|
+
-- a bubble, and \`unknown\` is the one that makes the NEXT gap visible in a query
|
|
5691
|
+
-- instead of in a complaint.
|
|
5692
|
+
--
|
|
5693
|
+
-- \`payload\` keeps the part that does not survive flattening: the vCard's parsed
|
|
5694
|
+
-- name and numbers, a media mimetype and size, a location's coordinates. Not
|
|
5695
|
+
-- the whole provider envelope \u2014 that carries decryption keys and a base64
|
|
5696
|
+
-- thumbnail, neither of which belongs in a row that every member of the tenant
|
|
5697
|
+
-- can read.
|
|
5698
|
+
--
|
|
5699
|
+
-- Deliberately NOT stored here: delivery status. It changes after the fact \u2014
|
|
5700
|
+
-- sent, delivered, read \u2014 and \`conversations-provider\`'s \`message_detail\` reads
|
|
5701
|
+
-- it live for that reason. \`payload\` is the shape of the message, which does
|
|
5702
|
+
-- not change once it arrives.
|
|
5703
|
+
--
|
|
5704
|
+
-- \u2500\u2500 why both are nullable, and nothing is backfilled \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5705
|
+
--
|
|
5706
|
+
-- Every row written before this migration has neither, and no default can
|
|
5707
|
+
-- invent them: \`message_type\` would have to guess, and guessing 'text' for the
|
|
5708
|
+
-- three empty bubbles already in the table would assert something false about
|
|
5709
|
+
-- exactly the rows this exists to fix. A reader that treats NULL as "written
|
|
5710
|
+
-- before 012" is correct; a reader that treats '' as text is the bug.
|
|
5711
|
+
--
|
|
5712
|
+
-- Table-level \`GRANT ALL ... TO authenticated\` already covers new columns, and
|
|
5713
|
+
-- the four tenant policies are row-scoped, so neither needs restating.
|
|
5714
|
+
--
|
|
5715
|
+
-- Idempotent and replay-safe.
|
|
5716
|
+
-- ---------------------------------------------------------------------------
|
|
5717
|
+
|
|
5718
|
+
ALTER TABLE public.plg_conversation_messages
|
|
5719
|
+
ADD COLUMN IF NOT EXISTS message_type text,
|
|
5720
|
+
ADD COLUMN IF NOT EXISTS payload jsonb;
|
|
5721
|
+
|
|
5722
|
+
COMMENT ON COLUMN public.plg_conversation_messages.message_type IS
|
|
5723
|
+
'What the provider sent: text, contact, image, video, audio, document, sticker, location, reaction, unknown. NULL means the row predates 012 (#504).';
|
|
5724
|
+
|
|
5725
|
+
COMMENT ON COLUMN public.plg_conversation_messages.payload IS
|
|
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).';
|
|
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
|
+
`;
|
|
5486
5794
|
var MIGRATIONS = [
|
|
5487
5795
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
5488
5796
|
{ id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO },
|
|
@@ -5495,7 +5803,9 @@ var MIGRATIONS = [
|
|
|
5495
5803
|
{ id: "008_dar_por_vista_e_uma_decisao", sql: MIGRATION_008_DAR_POR_VISTA_E_UMA_DECISAO },
|
|
5496
5804
|
{ id: "009_a_mensagem_inteira_atravessa_o_realtime", sql: MIGRATION_009_A_MENSAGEM_INTEIRA_ATRAVESSA_O_REALTIME },
|
|
5497
5805
|
{ id: "010_a_chegada_e_anunciada_por_conta", sql: MIGRATION_010_A_CHEGADA_E_ANUNCIADA_POR_CONTA },
|
|
5498
|
-
{ id: "011_o_anuncio_e_uma_campainha_nao_um_recado", sql: MIGRATION_011_O_ANUNCIO_E_UMA_CAMPAINHA_NAO_UM_RECADO }
|
|
5806
|
+
{ id: "011_o_anuncio_e_uma_campainha_nao_um_recado", sql: MIGRATION_011_O_ANUNCIO_E_UMA_CAMPAINHA_NAO_UM_RECADO },
|
|
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 }
|
|
5499
5809
|
];
|
|
5500
5810
|
|
|
5501
5811
|
// src/index.ts
|
|
@@ -5529,11 +5839,11 @@ function createConversationsPlugin(options) {
|
|
|
5529
5839
|
contactEntityDef: options?.contactEntityDef
|
|
5530
5840
|
};
|
|
5531
5841
|
const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
|
|
5532
|
-
const PageComponent = () =>
|
|
5842
|
+
const PageComponent = () => React12__default.createElement(ConversationsPage, { store: store2, config });
|
|
5533
5843
|
PageComponent.displayName = "ConversationsPage";
|
|
5534
|
-
const CampaignsPageComponent = () =>
|
|
5844
|
+
const CampaignsPageComponent = () => React12__default.createElement(CampaignsView);
|
|
5535
5845
|
CampaignsPageComponent.displayName = "ConversationsCampaignsPage";
|
|
5536
|
-
const TemplatesPageComponent = () =>
|
|
5846
|
+
const TemplatesPageComponent = () => React12__default.createElement(TemplatesView);
|
|
5537
5847
|
TemplatesPageComponent.displayName = "ConversationsTemplatesPage";
|
|
5538
5848
|
return {
|
|
5539
5849
|
id: "conversations",
|
|
@@ -5609,7 +5919,7 @@ function createConversationsPlugin(options) {
|
|
|
5609
5919
|
// quando ele serve para alguma coisa.
|
|
5610
5920
|
zone: "shell.topbar.end",
|
|
5611
5921
|
component: (() => {
|
|
5612
|
-
const W = () =>
|
|
5922
|
+
const W = () => React12__default.createElement(InboxWatcher, {
|
|
5613
5923
|
label: options?.navLabel ?? "Conversations",
|
|
5614
5924
|
section: options?.navSection ?? "main",
|
|
5615
5925
|
position: options?.navPosition ?? 1
|