@fayz-ai/plugin-conversations 0.16.1 → 0.17.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.
- package/dist/components/InboxWatcher.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.js +432 -60
- package/dist/index.js.map +1 -1
- package/dist/lib/campainha.d.ts +31 -0
- package/dist/lib/campainha.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/ConversationList.d.ts.map +1 -1
- package/dist/views/InboxView.d.ts.map +1 -1
- package/dist/views/useCaixaAoVivo.d.ts +27 -0
- package/dist/views/useCaixaAoVivo.d.ts.map +1 -0
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InboxWatcher.d.ts","sourceRoot":"","sources":["../../src/components/InboxWatcher.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"InboxWatcher.d.ts","sourceRoot":"","sources":["../../src/components/InboxWatcher.tsx"],"names":[],"mappings":"AAkGA,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,KAAwB,EAAE,OAAgB,EAAE,QAAY,EAAE,EAAE;IAChG,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;qCAEiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,QAiFA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAwBpD,MAAM,WAAW,2BAA2B;IAC1C,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC9C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAwBpD,MAAM,WAAW,2BAA2B;IAC1C,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC9C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA8CD,wBAAgB,mCAAmC,CACjD,MAAM,CAAC,EAAE,2BAA2B,GACnC,qBAAqB,CAuRvB"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React11 from 'react';
|
|
2
2
|
import React11__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
-
import { Loader2, MessageSquare, AlertTriangle, Pause, Play, Send, RefreshCw, Plus, Clock, CheckCircle2, Check, ShieldCheck, ShieldAlert, QrCode, Zap, ExternalLink, SquarePen, Globe, Mail, Instagram, Phone, Search, User, Inbox, ChevronLeft, Archive, PanelRight, X, MapPin, Tag, StickyNote, Link2, MoreVertical,
|
|
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';
|
|
4
4
|
import { toast, Button, Input, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, FilterChip, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
|
|
5
5
|
import { createConnectionStore, connectionRuns, connectionStatus, getActiveTenantId, getSupabaseClientOptional, useActiveTenantId, registerTranslations, useTranslation, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, errorMessage } from '@fayz-ai/core';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -386,6 +386,37 @@ var T = {
|
|
|
386
386
|
messages: "plg_conversation_messages",
|
|
387
387
|
channels: "plg_conversations_channels"
|
|
388
388
|
};
|
|
389
|
+
var porConta = /* @__PURE__ */ new Map();
|
|
390
|
+
function assinarChegada(tenantId, ouvinte) {
|
|
391
|
+
const supabase = getSupabaseClientOptional();
|
|
392
|
+
if (!supabase?.channel || !tenantId) return () => {
|
|
393
|
+
};
|
|
394
|
+
let assinatura = porConta.get(tenantId);
|
|
395
|
+
if (!assinatura) {
|
|
396
|
+
const ouvintes = /* @__PURE__ */ new Set();
|
|
397
|
+
const canal = supabase.channel(`conversas:${tenantId}`).on("broadcast", { event: "inbound" }, (msg2) => {
|
|
398
|
+
const carga = { conversationId: msg2?.payload?.conversationId };
|
|
399
|
+
for (const f of [...ouvintes]) {
|
|
400
|
+
try {
|
|
401
|
+
f(carga);
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}).subscribe();
|
|
406
|
+
assinatura = { canal, ouvintes };
|
|
407
|
+
porConta.set(tenantId, assinatura);
|
|
408
|
+
}
|
|
409
|
+
assinatura.ouvintes.add(ouvinte);
|
|
410
|
+
return () => {
|
|
411
|
+
const a = porConta.get(tenantId);
|
|
412
|
+
if (!a) return;
|
|
413
|
+
a.ouvintes.delete(ouvinte);
|
|
414
|
+
if (a.ouvintes.size === 0) {
|
|
415
|
+
porConta.delete(tenantId);
|
|
416
|
+
void getSupabaseClientOptional()?.removeChannel?.(a.canal);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
389
420
|
|
|
390
421
|
// src/types.ts
|
|
391
422
|
var CHANNEL_LABELS = {
|
|
@@ -397,6 +428,7 @@ var CHANNEL_LABELS = {
|
|
|
397
428
|
};
|
|
398
429
|
function isWaitingOnUs(conversation) {
|
|
399
430
|
if (conversation.status !== "open") return false;
|
|
431
|
+
if (conversation.acknowledgedAt && conversation.lastMessageAt && conversation.acknowledgedAt >= conversation.lastMessageAt) return false;
|
|
400
432
|
if (conversation.lastMessageDirection) return conversation.lastMessageDirection === "inbound";
|
|
401
433
|
return conversation.unreadCount > 0;
|
|
402
434
|
}
|
|
@@ -426,12 +458,39 @@ function tocar() {
|
|
|
426
458
|
} catch {
|
|
427
459
|
}
|
|
428
460
|
}
|
|
461
|
+
async function pedirPermissao() {
|
|
462
|
+
if (typeof Notification === "undefined") return false;
|
|
463
|
+
if (Notification.permission === "granted") return true;
|
|
464
|
+
if (Notification.permission === "denied") return false;
|
|
465
|
+
try {
|
|
466
|
+
return await Notification.requestPermission() === "granted";
|
|
467
|
+
} catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function avisarNoSistema(titulo, corpo, tag) {
|
|
472
|
+
try {
|
|
473
|
+
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
474
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") return;
|
|
475
|
+
const n = new Notification(titulo, { body: corpo, tag, renotify: true });
|
|
476
|
+
n.onclick = () => {
|
|
477
|
+
try {
|
|
478
|
+
window.focus();
|
|
479
|
+
n.close();
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
}
|
|
429
486
|
function InboxWatcher({ label, route = "/conversations", section = "main", position = 1 }) {
|
|
487
|
+
const tenantId = useActiveTenantId();
|
|
430
488
|
const [naoLidas, setNaoLidas] = React11.useState(0);
|
|
431
489
|
const podeTocar = React11.useRef(false);
|
|
432
490
|
React11.useEffect(() => {
|
|
433
491
|
const liberar = () => {
|
|
434
492
|
podeTocar.current = true;
|
|
493
|
+
void pedirPermissao();
|
|
435
494
|
};
|
|
436
495
|
for (const ev of ["pointerdown", "keydown", "touchstart"]) {
|
|
437
496
|
window.addEventListener(ev, liberar, { once: true, passive: true });
|
|
@@ -456,21 +515,23 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
|
|
|
456
515
|
void contar();
|
|
457
516
|
}, [contar]);
|
|
458
517
|
React11.useEffect(() => {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
518
|
+
if (!tenantId) return;
|
|
519
|
+
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
520
|
+
void contar();
|
|
521
|
+
if (podeTocar.current) tocar();
|
|
522
|
+
void (async () => {
|
|
523
|
+
const supabase = getSupabaseClientOptional();
|
|
524
|
+
let titulo = label;
|
|
525
|
+
let corpo = "";
|
|
526
|
+
if (conversationId && supabase) {
|
|
527
|
+
const { data } = await supabase.from(T.conversations).select("contact_name, last_message_preview").eq("id", conversationId).maybeSingle();
|
|
528
|
+
titulo = data?.contact_name || label;
|
|
529
|
+
corpo = String(data?.last_message_preview || "").replace(/\s+/g, " ").slice(0, 140);
|
|
530
|
+
}
|
|
531
|
+
avisarNoSistema(titulo, corpo, String(conversationId ?? "inbox"));
|
|
532
|
+
})();
|
|
533
|
+
});
|
|
534
|
+
}, [contar, tenantId, label]);
|
|
474
535
|
useNavContribution("conversations:unread", naoLidas > 0 ? [{ id: "conversations", label, route, badge: naoLidas, badgeTone: "destructive", section, position }] : []);
|
|
475
536
|
return null;
|
|
476
537
|
}
|
|
@@ -815,6 +876,7 @@ function ConversationList({ className }) {
|
|
|
815
876
|
search,
|
|
816
877
|
loading,
|
|
817
878
|
select,
|
|
879
|
+
markRead,
|
|
818
880
|
setChannelFilter,
|
|
819
881
|
setLeadOwnerFilter,
|
|
820
882
|
setSearch
|
|
@@ -885,48 +947,69 @@ function ConversationList({ className }) {
|
|
|
885
947
|
conversations.map((c) => {
|
|
886
948
|
const active = c.id === selectedId;
|
|
887
949
|
const unread = isWaitingOnUs(c);
|
|
888
|
-
return
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
/* @__PURE__ */
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
950
|
+
return (
|
|
951
|
+
// `div` em volta e não um botão dentro de outro: HTML não permite
|
|
952
|
+
// aninhar, e o navegador resolve isso silenciosamente movendo o
|
|
953
|
+
// interno para fora — a linha inteira deixa de ser clicável e
|
|
954
|
+
// ninguém entende por quê.
|
|
955
|
+
/* @__PURE__ */ jsxs("div", { className: "group relative", children: [
|
|
956
|
+
/* @__PURE__ */ jsxs(
|
|
957
|
+
"button",
|
|
958
|
+
{
|
|
959
|
+
onClick: () => select(c.id),
|
|
960
|
+
className: cn(
|
|
961
|
+
"flex w-full items-start gap-3 border-b border-border/50 px-3 py-3 text-left transition-colors",
|
|
962
|
+
// Um TOM da cor de marca, e não `bg-accent`.
|
|
963
|
+
//
|
|
964
|
+
// `bg-accent` resolve para quase preto neste tema, e a linha da
|
|
965
|
+
// conversa aberta ficava escura sobre escura. Trocar junto o
|
|
966
|
+
// `text-accent-foreground` deixava legível, mas pintava de
|
|
967
|
+
// preto a única linha que se está a ler — chamava mais atenção
|
|
968
|
+
// que a conversa.
|
|
969
|
+
//
|
|
970
|
+
// `bg-primary/10` é um véu: o texto continua com as cores dele,
|
|
971
|
+
// a seleção se lê como seleção e não como bloco, e funciona nos
|
|
972
|
+
// dois temas porque não presume nada sobre o fundo.
|
|
973
|
+
active ? "bg-primary/10" : "hover:bg-muted/50"
|
|
974
|
+
),
|
|
975
|
+
children: [
|
|
976
|
+
/* @__PURE__ */ jsx(Avatar, { name: c.contactName, accent: c.accent, channel: c.channel }),
|
|
977
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
978
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
979
|
+
/* @__PURE__ */ jsx("span", { className: cn("truncate text-sm text-foreground", unread ? "font-semibold" : "font-medium"), children: c.contactName }),
|
|
980
|
+
/* @__PURE__ */ jsx("span", { className: cn("shrink-0 text-[11px]", unread ? "font-semibold text-primary" : "text-muted-foreground"), children: relativeTime(c.lastMessageAt) })
|
|
981
|
+
] }),
|
|
982
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
|
|
983
|
+
/* @__PURE__ */ jsx(ChannelBadge, { channel: c.channel }),
|
|
984
|
+
c.status !== "open" && /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: t(`conversations.status.${c.status}`) }),
|
|
985
|
+
c.leadOwnerName && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-0.5 rounded-full bg-muted px-1.5 text-[10px] text-muted-foreground", children: [
|
|
986
|
+
/* @__PURE__ */ jsx(User, { className: "h-2.5 w-2.5" }),
|
|
987
|
+
c.leadOwnerName.split(" ")[0]
|
|
988
|
+
] })
|
|
989
|
+
] }),
|
|
990
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
|
|
991
|
+
/* @__PURE__ */ jsx("span", { className: cn("truncate text-xs", unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
|
|
992
|
+
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" })
|
|
993
|
+
] })
|
|
920
994
|
] })
|
|
921
|
-
]
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
995
|
+
]
|
|
996
|
+
}
|
|
997
|
+
),
|
|
998
|
+
unread && /* @__PURE__ */ jsx(
|
|
999
|
+
"button",
|
|
1000
|
+
{
|
|
1001
|
+
type: "button",
|
|
1002
|
+
title: t("conversations.markRead"),
|
|
1003
|
+
"aria-label": t("conversations.markRead"),
|
|
1004
|
+
onClick: (e) => {
|
|
1005
|
+
e.stopPropagation();
|
|
1006
|
+
void markRead(c.id);
|
|
1007
|
+
},
|
|
1008
|
+
className: "absolute right-2 top-2 hidden rounded-full p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground group-hover:block",
|
|
1009
|
+
children: /* @__PURE__ */ jsx(CheckCheck, { className: "h-3.5 w-3.5" })
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
] }, c.id)
|
|
930
1013
|
);
|
|
931
1014
|
})
|
|
932
1015
|
] })
|
|
@@ -1573,9 +1656,27 @@ function NewConversationPanel({
|
|
|
1573
1656
|
}
|
|
1574
1657
|
);
|
|
1575
1658
|
}
|
|
1659
|
+
function useCaixaAoVivo() {
|
|
1660
|
+
const tenantId = useActiveTenantId();
|
|
1661
|
+
const load = useConversationsStore((s) => s.load);
|
|
1662
|
+
const refreshMessages = useConversationsStore((s) => s.refreshMessages);
|
|
1663
|
+
const selectedId = useConversationsStore((s) => s.selectedId);
|
|
1664
|
+
const abertoRef = React11.useRef(selectedId);
|
|
1665
|
+
abertoRef.current = selectedId;
|
|
1666
|
+
React11.useEffect(() => {
|
|
1667
|
+
if (!tenantId) return;
|
|
1668
|
+
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
1669
|
+
void load();
|
|
1670
|
+
if (conversationId && conversationId === abertoRef.current) {
|
|
1671
|
+
void refreshMessages();
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
}, [tenantId, load, refreshMessages]);
|
|
1675
|
+
}
|
|
1576
1676
|
function InboxView() {
|
|
1577
1677
|
const t = useTranslation();
|
|
1578
1678
|
const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
|
|
1679
|
+
useCaixaAoVivo();
|
|
1579
1680
|
const isWidePanel = useMediaQuery("(min-width: 1280px)");
|
|
1580
1681
|
const [panelOpen, setPanelOpen] = React11__default.useState(false);
|
|
1581
1682
|
const [newOpen, setNewOpen] = React11__default.useState(false);
|
|
@@ -1954,6 +2055,7 @@ function mapConversation(r) {
|
|
|
1954
2055
|
// which is the mistake this one was not going to repeat).
|
|
1955
2056
|
lastMessageDirection: r.last_message_direction ?? void 0,
|
|
1956
2057
|
unreadCount: Number(r.unread_count ?? 0),
|
|
2058
|
+
acknowledgedAt: r.acknowledged_at ?? null,
|
|
1957
2059
|
status: r.status ?? "open",
|
|
1958
2060
|
assignedTo: r.assigned_to ?? void 0,
|
|
1959
2061
|
// Presentes só quando a leitura veio da view. Na escrita (que volta da
|
|
@@ -2156,7 +2258,11 @@ function createSupabaseConversationsProvider(config) {
|
|
|
2156
2258
|
},
|
|
2157
2259
|
async markRead(conversationId) {
|
|
2158
2260
|
const { error } = await client4().from(T.conversations).update({
|
|
2159
|
-
unread_count: 0
|
|
2261
|
+
unread_count: 0,
|
|
2262
|
+
// O instante é o que tira a bolinha. `unread_count` sozinho não
|
|
2263
|
+
// tirava — a fila olha a DIREÇÃO da última mensagem, e ela continua
|
|
2264
|
+
// sendo de entrada por mais que alguém leia.
|
|
2265
|
+
acknowledged_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2160
2266
|
}).eq("id", conversationId);
|
|
2161
2267
|
if (error) throw error;
|
|
2162
2268
|
},
|
|
@@ -2199,13 +2305,22 @@ function createConversationsStore(provider) {
|
|
|
2199
2305
|
set({ conversations, loading: false, selectedId });
|
|
2200
2306
|
if (selectedId) await get().select(selectedId);
|
|
2201
2307
|
},
|
|
2308
|
+
/** Dar por vista sem abrir — o caso do "ok, obrigado" que não pede resposta. */
|
|
2309
|
+
async markRead(id) {
|
|
2310
|
+
await provider.markRead(id);
|
|
2311
|
+
const agora = (/* @__PURE__ */ new Date()).toISOString();
|
|
2312
|
+
set((s) => ({
|
|
2313
|
+
conversations: s.conversations.map((c) => c.id === id ? { ...c, unreadCount: 0, acknowledgedAt: agora } : c)
|
|
2314
|
+
}));
|
|
2315
|
+
},
|
|
2202
2316
|
async select(id) {
|
|
2203
2317
|
set({ selectedId: id });
|
|
2204
2318
|
const messages = await provider.getMessages(id);
|
|
2205
2319
|
set({ messages });
|
|
2206
2320
|
await provider.markRead(id);
|
|
2321
|
+
const agora = (/* @__PURE__ */ new Date()).toISOString();
|
|
2207
2322
|
set((s) => ({
|
|
2208
|
-
conversations: s.conversations.map((c) => c.id === id ? { ...c, unreadCount: 0 } : c)
|
|
2323
|
+
conversations: s.conversations.map((c) => c.id === id ? { ...c, unreadCount: 0, acknowledgedAt: agora } : c)
|
|
2209
2324
|
}));
|
|
2210
2325
|
},
|
|
2211
2326
|
/** Relê as mensagens da conversa aberta, sem tocar em mais nada.
|
|
@@ -2324,6 +2439,7 @@ function createConversationsStore(provider) {
|
|
|
2324
2439
|
|
|
2325
2440
|
// src/locales/en.ts
|
|
2326
2441
|
var en = {
|
|
2442
|
+
"conversations.markRead": "Mark as read",
|
|
2327
2443
|
"conversations.title": "Conversations",
|
|
2328
2444
|
"conversations.subtitle": "Unified inbox across every channel",
|
|
2329
2445
|
// Conversation list
|
|
@@ -2419,6 +2535,7 @@ var en = {
|
|
|
2419
2535
|
|
|
2420
2536
|
// src/locales/pt-BR.ts
|
|
2421
2537
|
var ptBR = {
|
|
2538
|
+
"conversations.markRead": "Marcar como lida",
|
|
2422
2539
|
"conversations.title": "Conversas",
|
|
2423
2540
|
"conversations.subtitle": "Caixa de entrada unificada de todos os canais",
|
|
2424
2541
|
// Lista de conversas
|
|
@@ -5115,6 +5232,257 @@ CREATE OR REPLACE VIEW public.v_conversations WITH (security_invoker = true) AS
|
|
|
5115
5232
|
COMMENT ON VIEW public.v_conversations IS
|
|
5116
5233
|
'A conversa com o DONO DO LEAD junto \u2014 diferente de \`assigned_to\`, que \xE9 quem atende a conversa. Derivado de \`v_leads\` para n\xE3o virar uma segunda verdade que envelhece (007).';
|
|
5117
5234
|
`;
|
|
5235
|
+
var MIGRATION_008_DAR_POR_VISTA_E_UMA_DECISAO = `-- ---------------------------------------------------------------------------
|
|
5236
|
+
-- 008_dar_por_vista_e_uma_decisao.sql
|
|
5237
|
+
--
|
|
5238
|
+
-- \u2500\u2500 o buraco \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5239
|
+
--
|
|
5240
|
+
-- A bolinha vermelha da caixa n\xE3o \xE9 "n\xE3o lido": \xE9 \`isWaitingOnUs\`, que olha a
|
|
5241
|
+
-- DIRE\xC7\xC3O da \xFAltima mensagem. Se a \xFAltima \xE9 de entrada, a conversa espera
|
|
5242
|
+
-- resposta \u2014 e continua esperando por mais que algu\xE9m abra e leia.
|
|
5243
|
+
--
|
|
5244
|
+
-- Isso est\xE1 certo enquanto a pergunta \xE9 "o que falta responder". Mas deixa sem
|
|
5245
|
+
-- sa\xEDda o caso comum: a pessoa leu, decidiu que n\xE3o cabe resposta ("ok",
|
|
5246
|
+
-- "obrigado", um \xE1udio que j\xE1 foi tratado por telefone) e n\xE3o tem como tirar a
|
|
5247
|
+
-- conversa da fila. \`unread_count = 0\` n\xE3o resolve, porque n\xE3o \xE9 ele que
|
|
5248
|
+
-- acende a bolinha.
|
|
5249
|
+
--
|
|
5250
|
+
-- \u2500\u2500 por que um instante, e n\xE3o um booleano \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
|
|
5251
|
+
--
|
|
5252
|
+
-- \`lida boolean\` exigiria algu\xE9m lembrar de apag\xE1-la quando chegasse mensagem
|
|
5253
|
+
-- nova \u2014 e o dia em que esquecessem, a conversa sumiria da fila com o cliente
|
|
5254
|
+
-- esperando. \xC9 o pior defeito poss\xEDvel numa caixa de entrada: silencioso, e do
|
|
5255
|
+
-- lado de perder trabalho.
|
|
5256
|
+
--
|
|
5257
|
+
-- Um INSTANTE se compara com \`last_message_at\` e se invalida sozinho: deu por
|
|
5258
|
+
-- vista \xE0s 14h, chegou mensagem \xE0s 15h, a conversa volta \xE0 fila sem ningu\xE9m
|
|
5259
|
+
-- fazer nada. A regra \xE9 uma compara\xE7\xE3o, n\xE3o um estado a manter.
|
|
5260
|
+
--
|
|
5261
|
+
-- \u2500\u2500 o que N\xC3O muda \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\u2500
|
|
5262
|
+
--
|
|
5263
|
+
-- \`unread_count\` continua onde est\xE1. Ele conta mensagens e serve ao contador
|
|
5264
|
+
-- do menu; esta coluna responde outra pergunta ("algu\xE9m j\xE1 decidiu sobre
|
|
5265
|
+
-- isto?"). Colapsar as duas faria a contagem do menu mentir no dia em que
|
|
5266
|
+
-- algu\xE9m desse por vista sem ler.
|
|
5267
|
+
--
|
|
5268
|
+
-- Idempotente e replay-safe.
|
|
5269
|
+
-- ---------------------------------------------------------------------------
|
|
5270
|
+
|
|
5271
|
+
ALTER TABLE public.plg_conversations
|
|
5272
|
+
ADD COLUMN IF NOT EXISTS acknowledged_at timestamptz;
|
|
5273
|
+
|
|
5274
|
+
COMMENT ON COLUMN public.plg_conversations.acknowledged_at IS
|
|
5275
|
+
'Quando algu\xE9m deu a conversa por vista. Vale enquanto for POSTERIOR a \`last_message_at\` \u2014 mensagem nova devolve a conversa \xE0 fila sozinha (008).';
|
|
5276
|
+
|
|
5277
|
+
-- A fila \xE9 lida por \`last_message_at\` e por esta coluna juntas.
|
|
5278
|
+
CREATE INDEX IF NOT EXISTS plg_conversations_fila_idx
|
|
5279
|
+
ON public.plg_conversations (tenant_id, status, last_message_at DESC)
|
|
5280
|
+
WHERE status = 'open';
|
|
5281
|
+
`;
|
|
5282
|
+
var MIGRATION_009_A_MENSAGEM_INTEIRA_ATRAVESSA_O_REALTIME = `-- ---------------------------------------------------------------------------
|
|
5283
|
+
-- 009_a_mensagem_inteira_atravessa_o_realtime.sql
|
|
5284
|
+
--
|
|
5285
|
+
-- \u2500\u2500 o buraco \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5286
|
+
--
|
|
5287
|
+
-- O vigia da caixa assinava \`plg_conversation_messages\`, recebia \`SUBSCRIBED\`
|
|
5288
|
+
-- do servidor, e nunca recebia um \xFAnico evento. Nenhum erro, nenhum aviso: a
|
|
5289
|
+
-- assinatura dizia que estava tudo bem e n\xE3o chegava nada.
|
|
5290
|
+
--
|
|
5291
|
+
-- A causa \xE9 a combina\xE7\xE3o de duas coisas que, sozinhas, est\xE3o certas:
|
|
5292
|
+
--
|
|
5293
|
+
-- 1. a tabela tem RLS ligada, como toda tabela desta base;
|
|
5294
|
+
-- 2. a tabela estava com \`REPLICA IDENTITY\` no padr\xE3o \u2014 s\xF3 a chave prim\xE1ria.
|
|
5295
|
+
--
|
|
5296
|
+
-- O Realtime do Supabase aplica a POL\xCDTICA DE LEITURA do usu\xE1rio a cada evento
|
|
5297
|
+
-- antes de entreg\xE1-lo. Para isso ele precisa da linha inteira: a pol\xEDtica fala
|
|
5298
|
+
-- de \`tenant_id\`, e \`tenant_id\` n\xE3o \xE9 a chave prim\xE1ria. Com s\xF3 a PK no WAL, n\xE3o
|
|
5299
|
+
-- h\xE1 o que avaliar \u2014 e o evento \xE9 descartado sem ru\xEDdo.
|
|
5300
|
+
--
|
|
5301
|
+
-- \`plg_conversations\` j\xE1 estava \`FULL\` (006). A tabela de MENSAGENS ficou de
|
|
5302
|
+
-- fora daquela migration, e \xE9 justamente ela que o vigia escuta.
|
|
5303
|
+
--
|
|
5304
|
+
-- \u2500\u2500 por que isso \xE9 caro e ainda assim certo \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
|
|
5305
|
+
--
|
|
5306
|
+
-- \`FULL\` faz o WAL carregar a linha inteira em cada UPDATE e DELETE, n\xE3o s\xF3 as
|
|
5307
|
+
-- colunas mudadas. Numa tabela de mensagens isso \xE9 peso real.
|
|
5308
|
+
--
|
|
5309
|
+
-- A alternativa \u2014 desligar a RLS para o Realtime enxergar \u2014 trocaria um custo
|
|
5310
|
+
-- de disco por um vazamento: as mensagens de uma casa chegariam ao navegador
|
|
5311
|
+
-- de outra. Entre pagar WAL e vazar conversa, paga-se o WAL.
|
|
5312
|
+
--
|
|
5313
|
+
-- Idempotente e replay-safe.
|
|
5314
|
+
-- ---------------------------------------------------------------------------
|
|
5315
|
+
|
|
5316
|
+
ALTER TABLE public.plg_conversation_messages REPLICA IDENTITY FULL;
|
|
5317
|
+
|
|
5318
|
+
-- Garante a publica\xE7\xE3o tamb\xE9m \u2014 uma tabela com identidade cheia e fora da
|
|
5319
|
+
-- publica\xE7\xE3o continua sem emitir nada, e o sintoma seria id\xEAntico.
|
|
5320
|
+
DO $$
|
|
5321
|
+
BEGIN
|
|
5322
|
+
IF NOT EXISTS (
|
|
5323
|
+
SELECT 1 FROM pg_publication_tables
|
|
5324
|
+
WHERE pubname = 'supabase_realtime'
|
|
5325
|
+
AND schemaname = 'public'
|
|
5326
|
+
AND tablename = 'plg_conversation_messages'
|
|
5327
|
+
) THEN
|
|
5328
|
+
EXECUTE 'ALTER PUBLICATION supabase_realtime ADD TABLE public.plg_conversation_messages';
|
|
5329
|
+
END IF;
|
|
5330
|
+
END $$;
|
|
5331
|
+
`;
|
|
5332
|
+
var MIGRATION_010_A_CHEGADA_E_ANUNCIADA_POR_CONTA = `-- ---------------------------------------------------------------------------
|
|
5333
|
+
-- 010_a_chegada_e_anunciada_por_conta.sql
|
|
5334
|
+
--
|
|
5335
|
+
-- \u2500\u2500 por que \`postgres_changes\` nunca funcionou aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5336
|
+
--
|
|
5337
|
+
-- O vigia assinava a TABELA de mensagens e recebia \`SUBSCRIBED\` do servidor.
|
|
5338
|
+
-- Nenhum evento chegava. Nenhum erro, nenhum aviso \u2014 a assinatura dizia que
|
|
5339
|
+
-- estava tudo bem.
|
|
5340
|
+
--
|
|
5341
|
+
-- O Realtime aplica a pol\xEDtica de leitura a cada evento antes de entreg\xE1-lo. A
|
|
5342
|
+
-- pol\xEDtica desta base resolve o tenant por, nesta ordem:
|
|
5343
|
+
--
|
|
5344
|
+
-- \u2460 a claim \`tenant_id\` do JWT
|
|
5345
|
+
-- \u2461 o CABE\xC7ALHO da requisi\xE7\xE3o (\`app.requested_tenant()\`)
|
|
5346
|
+
-- \u2462 a filia\xE7\xE3o \xFAnica, quando a pessoa s\xF3 tem uma
|
|
5347
|
+
--
|
|
5348
|
+
-- O Realtime manda o JWT e N\xC3O manda cabe\xE7alho. E o JWT desta casa n\xE3o carrega
|
|
5349
|
+
-- \`tenant_id\` \u2014 a conta ativa \xE9 escolhida por cabe\xE7alho, porque a mesma pessoa
|
|
5350
|
+
-- atende sete contas. Sobra \u2462, que devolve nulo para quem tem mais de uma.
|
|
5351
|
+
--
|
|
5352
|
+
-- Resultado: a pol\xEDtica recusa, o evento \xE9 descartado em sil\xEAncio, e o defeito
|
|
5353
|
+
-- parece "o realtime n\xE3o funciona" quando na verdade \xE9 "o realtime n\xE3o sabe em
|
|
5354
|
+
-- que conta voc\xEA est\xE1".
|
|
5355
|
+
--
|
|
5356
|
+
-- \u2500\u2500 a sa\xEDda: anunciar, em vez de espiar a tabela \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5357
|
+
--
|
|
5358
|
+
-- Broadcast n\xE3o passa pela RLS da tabela: o servidor entrega a quem est\xE1 no
|
|
5359
|
+
-- T\xD3PICO. Ent\xE3o o t\xF3pico carrega a conta no nome \u2014 \`conversas:<tenant>\` \u2014 e
|
|
5360
|
+
-- quem escuta j\xE1 teve de saber em que conta est\xE1 para montar o nome.
|
|
5361
|
+
--
|
|
5362
|
+
-- Isso inverte a depend\xEAncia de forma saud\xE1vel: em vez de o banco adivinhar o
|
|
5363
|
+
-- contexto do navegador, o navegador declara o contexto que j\xE1 tem.
|
|
5364
|
+
--
|
|
5365
|
+
-- \u2500\u2500 o que vai no an\xFAncio \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
|
|
5366
|
+
--
|
|
5367
|
+
-- O M\xCDNIMO para decidir: id da conversa, canal, e um trecho. N\xE3o a mensagem
|
|
5368
|
+
-- inteira \u2014 um broadcast \xE9 entregue a todos do t\xF3pico, e mandar o corpo
|
|
5369
|
+
-- completo por ali seria espalhar conversa por um caminho que n\xE3o \xE9 o que
|
|
5370
|
+
-- guarda conversa. Quem quiser o texto l\xEA a tabela, com RLS, como sempre.
|
|
5371
|
+
--
|
|
5372
|
+
-- Idempotente e replay-safe.
|
|
5373
|
+
-- ---------------------------------------------------------------------------
|
|
5374
|
+
|
|
5375
|
+
CREATE OR REPLACE FUNCTION public.plg_conversations_anunciar_chegada()
|
|
5376
|
+
RETURNS trigger
|
|
5377
|
+
LANGUAGE plpgsql
|
|
5378
|
+
SECURITY DEFINER
|
|
5379
|
+
SET search_path = ''
|
|
5380
|
+
AS $$
|
|
5381
|
+
DECLARE
|
|
5382
|
+
v_nome text;
|
|
5383
|
+
BEGIN
|
|
5384
|
+
-- S\xF3 o que CHEGA. A resposta que o operador manda tamb\xE9m \xE9 um INSERT, e
|
|
5385
|
+
-- anunci\xE1-la faria o sistema apitar para quem acabou de digitar.
|
|
5386
|
+
IF NEW.direction IS DISTINCT FROM 'inbound' THEN
|
|
5387
|
+
RETURN NEW;
|
|
5388
|
+
END IF;
|
|
5389
|
+
|
|
5390
|
+
SELECT c.contact_name INTO v_nome
|
|
5391
|
+
FROM public.plg_conversations c
|
|
5392
|
+
WHERE c.id = NEW.conversation_id;
|
|
5393
|
+
|
|
5394
|
+
PERFORM realtime.send(
|
|
5395
|
+
jsonb_build_object(
|
|
5396
|
+
'conversationId', NEW.conversation_id,
|
|
5397
|
+
'channel', NEW.channel,
|
|
5398
|
+
'contactName', coalesce(v_nome, ''),
|
|
5399
|
+
-- Um trecho, n\xE3o o corpo: o suficiente para a notifica\xE7\xE3o do sistema
|
|
5400
|
+
-- dizer do que se trata sem espalhar a conversa pelo broadcast.
|
|
5401
|
+
'preview', left(coalesce(NEW.body, ''), 120),
|
|
5402
|
+
'at', NEW.at
|
|
5403
|
+
),
|
|
5404
|
+
'inbound',
|
|
5405
|
+
'conversas:' || NEW.tenant_id::text,
|
|
5406
|
+
-- Privado: exige que quem assina esteja autenticado. P\xFAblico deixaria
|
|
5407
|
+
-- qualquer um com o nome do t\xF3pico ouvir a chegada.
|
|
5408
|
+
true
|
|
5409
|
+
);
|
|
5410
|
+
RETURN NEW;
|
|
5411
|
+
END $$;
|
|
5412
|
+
|
|
5413
|
+
REVOKE ALL ON FUNCTION public.plg_conversations_anunciar_chegada() FROM public, anon, authenticated;
|
|
5414
|
+
|
|
5415
|
+
DROP TRIGGER IF EXISTS plg_conversations_anunciar_chegada ON public.plg_conversation_messages;
|
|
5416
|
+
CREATE TRIGGER plg_conversations_anunciar_chegada
|
|
5417
|
+
AFTER INSERT ON public.plg_conversation_messages
|
|
5418
|
+
FOR EACH ROW EXECUTE FUNCTION public.plg_conversations_anunciar_chegada();
|
|
5419
|
+
|
|
5420
|
+
COMMENT ON FUNCTION public.plg_conversations_anunciar_chegada() IS
|
|
5421
|
+
'Anuncia a chegada no t\xF3pico \`conversas:<tenant>\`. Broadcast e n\xE3o \`postgres_changes\` porque a RLS da tabela resolve o tenant por cabe\xE7alho, e o Realtime n\xE3o manda cabe\xE7alho (010).';
|
|
5422
|
+
`;
|
|
5423
|
+
var MIGRATION_011_O_ANUNCIO_E_UMA_CAMPAINHA_NAO_UM_RECADO = `-- ---------------------------------------------------------------------------
|
|
5424
|
+
-- 011_o_anuncio_e_uma_campainha_nao_um_recado.sql
|
|
5425
|
+
--
|
|
5426
|
+
-- \u2500\u2500 por que a 010 n\xE3o podia ficar como estava \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
|
|
5427
|
+
--
|
|
5428
|
+
-- A 010 anunciava num t\xF3pico PRIVADO e mandava, no an\xFAncio, o nome do contato e
|
|
5429
|
+
-- um trecho da mensagem. T\xF3pico privado exige pol\xEDtica em \`realtime.messages\` \u2014
|
|
5430
|
+
-- e essa tabela \xE9 do Supabase, dona \`supabase_realtime_admin\`, um papel que
|
|
5431
|
+
-- esta conex\xE3o n\xE3o pode assumir:
|
|
5432
|
+
--
|
|
5433
|
+
-- ERROR: 42501: permission denied to set role "supabase_realtime_admin"
|
|
5434
|
+
--
|
|
5435
|
+
-- Sem a pol\xEDtica, o servidor recusa a assinatura e nada chega. Com o t\xF3pico
|
|
5436
|
+
-- p\xFAblico, qualquer um que soubesse o nome do t\xF3pico leria o trecho da
|
|
5437
|
+
-- conversa \u2014 o que n\xE3o se faz.
|
|
5438
|
+
--
|
|
5439
|
+
-- \u2500\u2500 a sa\xEDda: separar o AVISO do CONTE\xDADO \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
|
|
5440
|
+
--
|
|
5441
|
+
-- O an\xFAncio passa a ser uma campainha: diz que chegou algo e em que conversa,
|
|
5442
|
+
-- e mais nada. Nome e texto o navegador busca pela via de sempre \u2014 a tabela,
|
|
5443
|
+
-- com RLS, onde o cabe\xE7alho da conta existe e a pol\xEDtica funciona.
|
|
5444
|
+
--
|
|
5445
|
+
-- O t\xF3pico continua carregando o uuid da conta no nome, que \xE9 o que separa uma
|
|
5446
|
+
-- casa da outra. Ele n\xE3o \xE9 adivinh\xE1vel, e mesmo que fosse, o que se ouviria \xE9
|
|
5447
|
+
-- "chegou mensagem", sem nome nem texto. O sigilo fica onde sempre esteve: na
|
|
5448
|
+
-- leitura da tabela.
|
|
5449
|
+
--
|
|
5450
|
+
-- \xC9 uma troca deliberada de superf\xEDcie: em vez de tentar proteger um caminho
|
|
5451
|
+
-- novo, n\xE3o se p\xF5e nada sens\xEDvel nele.
|
|
5452
|
+
--
|
|
5453
|
+
-- Idempotente e replay-safe.
|
|
5454
|
+
-- ---------------------------------------------------------------------------
|
|
5455
|
+
|
|
5456
|
+
CREATE OR REPLACE FUNCTION public.plg_conversations_anunciar_chegada()
|
|
5457
|
+
RETURNS trigger
|
|
5458
|
+
LANGUAGE plpgsql
|
|
5459
|
+
SECURITY DEFINER
|
|
5460
|
+
SET search_path = ''
|
|
5461
|
+
AS $$
|
|
5462
|
+
BEGIN
|
|
5463
|
+
-- S\xF3 o que CHEGA. A resposta que o operador manda tamb\xE9m \xE9 um INSERT, e
|
|
5464
|
+
-- anunci\xE1-la faria o sistema apitar para quem acabou de digitar.
|
|
5465
|
+
IF NEW.direction IS DISTINCT FROM 'inbound' THEN
|
|
5466
|
+
RETURN NEW;
|
|
5467
|
+
END IF;
|
|
5468
|
+
|
|
5469
|
+
PERFORM realtime.send(
|
|
5470
|
+
-- S\xD3 o id da conversa. Nem nome, nem trecho, nem corpo: quem ouve vai
|
|
5471
|
+
-- buscar isso na tabela, com a pol\xEDtica de leitura no meio do caminho.
|
|
5472
|
+
-- Um uuid sozinho n\xE3o conta nada a quem n\xE3o pode ler a conversa.
|
|
5473
|
+
jsonb_build_object('conversationId', NEW.conversation_id),
|
|
5474
|
+
'inbound',
|
|
5475
|
+
'conversas:' || NEW.tenant_id::text,
|
|
5476
|
+
-- P\xDABLICO, e por isso o payload \xE9 vazio de conte\xFAdo. Privado exigiria uma
|
|
5477
|
+
-- pol\xEDtica numa tabela que n\xE3o \xE9 nossa.
|
|
5478
|
+
false
|
|
5479
|
+
);
|
|
5480
|
+
RETURN NEW;
|
|
5481
|
+
END $$;
|
|
5482
|
+
|
|
5483
|
+
COMMENT ON FUNCTION public.plg_conversations_anunciar_chegada() IS
|
|
5484
|
+
'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
|
+
`;
|
|
5118
5486
|
var MIGRATIONS = [
|
|
5119
5487
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
5120
5488
|
{ id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO },
|
|
@@ -5123,7 +5491,11 @@ var MIGRATIONS = [
|
|
|
5123
5491
|
{ 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 },
|
|
5124
5492
|
{ id: "005_a_conversa_conta_ao_funil_que_houve_contato", sql: MIGRATION_005_A_CONVERSA_CONTA_AO_FUNIL_QUE_HOUVE_CONTATO },
|
|
5125
5493
|
{ id: "006_a_caixa_avisa_que_chegou", sql: MIGRATION_006_A_CAIXA_AVISA_QUE_CHEGOU },
|
|
5126
|
-
{ id: "007_a_conversa_diz_de_quem_e_o_lead", sql: MIGRATION_007_A_CONVERSA_DIZ_DE_QUEM_E_O_LEAD }
|
|
5494
|
+
{ id: "007_a_conversa_diz_de_quem_e_o_lead", sql: MIGRATION_007_A_CONVERSA_DIZ_DE_QUEM_E_O_LEAD },
|
|
5495
|
+
{ id: "008_dar_por_vista_e_uma_decisao", sql: MIGRATION_008_DAR_POR_VISTA_E_UMA_DECISAO },
|
|
5496
|
+
{ id: "009_a_mensagem_inteira_atravessa_o_realtime", sql: MIGRATION_009_A_MENSAGEM_INTEIRA_ATRAVESSA_O_REALTIME },
|
|
5497
|
+
{ 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 }
|
|
5127
5499
|
];
|
|
5128
5500
|
|
|
5129
5501
|
// src/index.ts
|