@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.5

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/index.js CHANGED
@@ -15,10 +15,12 @@ import {
15
15
  MessageComposer,
16
16
  StatusTicks,
17
17
  cn,
18
+ formatDateTime,
18
19
  formatFileSize,
19
20
  formatTimestamp,
21
+ isSameDay,
20
22
  useConversationLocales
21
- } from "./chunk-4R6Y43DQ.js";
23
+ } from "./chunk-YWITIIHD.js";
22
24
  import {
23
25
  htmlToWA,
24
26
  parseWhatsAppFormatting,
@@ -1148,6 +1150,7 @@ function isWindowBlocking(window2) {
1148
1150
 
1149
1151
  // src/ConversationDocumentsPanel.tsx
1150
1152
  import { useState as useState8 } from "react";
1153
+ import { ArrowUpDown, Bot, Download, Eye, Users } from "lucide-react";
1151
1154
 
1152
1155
  // src/providers/ConversationsProvider.tsx
1153
1156
  import { createContext as createContext2, useContext as useContext2 } from "react";
@@ -1190,6 +1193,17 @@ function useAsyncResource(fetcher, deps) {
1190
1193
  return { data, loading, error, refetch: load };
1191
1194
  }
1192
1195
 
1196
+ // src/lib/paginated.ts
1197
+ function conversationsOf(result) {
1198
+ return Array.isArray(result) ? result : result.conversations;
1199
+ }
1200
+ function documentsOf(result) {
1201
+ return Array.isArray(result) ? result : result.documents;
1202
+ }
1203
+ function totalOf(result) {
1204
+ return Array.isArray(result) ? result.length : result.total;
1205
+ }
1206
+
1193
1207
  // src/hooks/useConversationDocuments.ts
1194
1208
  function useConversationDocuments(conversationId, params) {
1195
1209
  const context = useConversations();
@@ -1199,25 +1213,49 @@ function useConversationDocuments(conversationId, params) {
1199
1213
  const { api } = context;
1200
1214
  const { data, loading, error, refetch } = useAsyncResource(
1201
1215
  () => conversationId ? api.getDocuments(conversationId, params) : Promise.resolve([]),
1202
- [conversationId, params?.search, params?.page]
1216
+ [conversationId, params?.search, params?.page, params?.limit, params?.source, params?.sortDirection]
1203
1217
  );
1204
- return { documents: data ?? [], loading, error, refetch };
1218
+ if (data === void 0) {
1219
+ return { documents: [], total: 0, loading, error, refetch };
1220
+ }
1221
+ return { documents: documentsOf(data), total: totalOf(data), loading, error, refetch };
1205
1222
  }
1206
1223
 
1207
1224
  // src/ConversationDocumentsPanel.tsx
1208
1225
  import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1226
+ var DOCUMENT_SOURCE_FILTER = {
1227
+ ALL: "all",
1228
+ CUSTOMER: "customer",
1229
+ TEAM: "team"
1230
+ };
1231
+ var TEAM_SOURCES = /* @__PURE__ */ new Set(["agent", "bot"]);
1209
1232
  var DEFAULT_CONVERSATION_DOCUMENTS_LABELS = {
1210
1233
  toggle: "\u{1F4CE} Arquivos",
1211
1234
  title: "Arquivos da conversa",
1212
- searchPlaceholder: "Buscar arquivo...",
1235
+ noResults: "Nenhum arquivo encontrado para os filtros aplicados",
1236
+ view: "Visualizar",
1237
+ sourceFilterAll: "Todas as origens",
1238
+ sourceFilterCustomer: "Cliente",
1239
+ sourceFilterTeam: "Equipe",
1240
+ sortMostRecent: "Mais recentes",
1241
+ sortOldest: "Mais antigos",
1242
+ clearFilters: "Limpar filtros",
1243
+ selectAll: "Selecionar todos desta p\xE1gina",
1244
+ downloadSelected: (count) => `Baixar ${count} selecionado${count === 1 ? "" : "s"} (.zip)`,
1245
+ archiveFailed: "N\xE3o foi poss\xEDvel montar o arquivo compactado.",
1246
+ total: (count) => `${count} arquivo${count === 1 ? "" : "s"}`,
1247
+ page: (current, last) => `${current} / ${last}`,
1248
+ searchPlaceholder: "Buscar por nome do arquivo",
1213
1249
  empty: "Nenhum arquivo nesta conversa.",
1214
1250
  loading: "Carregando arquivos\u2026",
1215
1251
  failure: "N\xE3o foi poss\xEDvel carregar os arquivos.",
1216
1252
  download: "Baixar"
1217
1253
  };
1254
+ var DEFAULT_PER_PAGE = 10;
1218
1255
  function ConversationDocumentsPanel({
1219
1256
  conversationId,
1220
1257
  open,
1258
+ perPage = DEFAULT_PER_PAGE,
1221
1259
  labels: labelsOverride,
1222
1260
  className,
1223
1261
  classNames
@@ -1225,34 +1263,253 @@ function ConversationDocumentsPanel({
1225
1263
  const labels = { ...DEFAULT_CONVERSATION_DOCUMENTS_LABELS, ...labelsOverride };
1226
1264
  const context = useConversations();
1227
1265
  const [search, setSearch] = useState8("");
1228
- const { documents, loading, error } = useConversationDocuments(open ? conversationId : void 0, { search });
1229
- async function handleDownload(uploadId) {
1230
- const url = await context?.api.getDocumentUrl(uploadId);
1266
+ const [sourceFilter, setSourceFilter] = useState8(DOCUMENT_SOURCE_FILTER.ALL);
1267
+ const [sortDirection, setSortDirection] = useState8("desc");
1268
+ const [page, setPage] = useState8(1);
1269
+ const [selectedIds, setSelectedIds] = useState8([]);
1270
+ const [archiveError, setArchiveError] = useState8(false);
1271
+ const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
1272
+ const { documents, total, loading, error } = useConversationDocuments(open ? conversationId : void 0, {
1273
+ search,
1274
+ page,
1275
+ limit: perPage,
1276
+ sortDirection,
1277
+ ...sourceFilter === DOCUMENT_SOURCE_FILTER.ALL ? {} : { source: sourceFilter }
1278
+ });
1279
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
1280
+ function applyFilter(change) {
1281
+ change();
1282
+ setPage(1);
1283
+ }
1284
+ async function handleOpen(uploadId, disposition) {
1285
+ const url = await context?.api.getDocumentUrl(uploadId, disposition);
1231
1286
  if (url) window.open(url, "_blank", "noopener,noreferrer");
1232
1287
  }
1288
+ const canArchive = typeof context?.api.downloadDocumentsArchive === "function";
1289
+ const pageIds = documents.map((document2) => document2.id);
1290
+ const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selectedIds.includes(id));
1291
+ function toggleSelected(uploadId) {
1292
+ setArchiveError(false);
1293
+ setSelectedIds(
1294
+ (current) => current.includes(uploadId) ? current.filter((id) => id !== uploadId) : [...current, uploadId]
1295
+ );
1296
+ }
1297
+ function toggleAllOnPage() {
1298
+ setArchiveError(false);
1299
+ setSelectedIds(
1300
+ (current) => allOnPageSelected ? current.filter((id) => !pageIds.includes(id)) : [...current, ...pageIds.filter((id) => !current.includes(id))]
1301
+ );
1302
+ }
1303
+ async function handleDownloadSelected() {
1304
+ const archive = context?.api.downloadDocumentsArchive;
1305
+ if (!archive || selectedIds.length === 0) return;
1306
+ setArchiveError(false);
1307
+ try {
1308
+ const blob = await archive(conversationId, selectedIds);
1309
+ const url = URL.createObjectURL(blob);
1310
+ const anchor = document.createElement("a");
1311
+ anchor.href = url;
1312
+ anchor.download = `conversa-${conversationId}.zip`;
1313
+ anchor.click();
1314
+ URL.revokeObjectURL(url);
1315
+ setSelectedIds([]);
1316
+ } catch {
1317
+ setArchiveError(true);
1318
+ }
1319
+ }
1233
1320
  if (!open) return null;
1234
1321
  return /* @__PURE__ */ jsx15("div", { className: cn("border-b", classNames?.root, className), children: /* @__PURE__ */ jsxs12("section", { className: cn("px-4 py-3", classNames?.body), children: [
1235
- /* @__PURE__ */ jsx15("p", { className: "mb-2 text-sm font-medium", children: labels.title }),
1236
- /* @__PURE__ */ jsx15(
1237
- "input",
1322
+ /* @__PURE__ */ jsx15("p", { className: cn("mb-2 text-sm font-medium", classNames?.title), children: labels.title }),
1323
+ /* @__PURE__ */ jsxs12("div", { className: cn("mb-2 flex flex-wrap items-center gap-2 border-b pb-2", classNames?.filters), children: [
1324
+ /* @__PURE__ */ jsx15(
1325
+ "input",
1326
+ {
1327
+ type: "search",
1328
+ value: search,
1329
+ onChange: (event) => applyFilter(() => setSearch(event.target.value)),
1330
+ placeholder: labels.searchPlaceholder,
1331
+ "aria-label": labels.searchPlaceholder,
1332
+ className: cn("w-full rounded-md border px-3 py-2 text-sm sm:w-52", classNames?.search)
1333
+ }
1334
+ ),
1335
+ /* @__PURE__ */ jsxs12(
1336
+ "select",
1337
+ {
1338
+ value: sourceFilter,
1339
+ onChange: (event) => applyFilter(() => setSourceFilter(event.target.value)),
1340
+ "aria-label": labels.sourceFilterAll,
1341
+ className: cn("w-full rounded-md border px-2 py-2 text-sm sm:w-36", classNames?.sourceSelect),
1342
+ children: [
1343
+ /* @__PURE__ */ jsx15("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
1344
+ /* @__PURE__ */ jsx15("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
1345
+ /* @__PURE__ */ jsx15("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
1346
+ ]
1347
+ }
1348
+ ),
1349
+ /* @__PURE__ */ jsxs12(
1350
+ "button",
1351
+ {
1352
+ type: "button",
1353
+ onClick: () => applyFilter(() => setSortDirection(sortDirection === "desc" ? "asc" : "desc")),
1354
+ className: cn("cv-header-action inline-flex items-center gap-1", classNames?.sortButton),
1355
+ children: [
1356
+ /* @__PURE__ */ jsx15(ArrowUpDown, { size: 14 }),
1357
+ sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest
1358
+ ]
1359
+ }
1360
+ ),
1361
+ hasFilters ? /* @__PURE__ */ jsx15(
1362
+ "button",
1363
+ {
1364
+ type: "button",
1365
+ onClick: () => applyFilter(() => {
1366
+ setSearch("");
1367
+ setSourceFilter(DOCUMENT_SOURCE_FILTER.ALL);
1368
+ setSortDirection("desc");
1369
+ }),
1370
+ className: cn("cv-header-action", classNames?.clearButton),
1371
+ children: labels.clearFilters
1372
+ }
1373
+ ) : null
1374
+ ] }),
1375
+ loading ? /* @__PURE__ */ jsx15("p", { className: cn("text-xs text-gray-500", classNames?.status), children: labels.loading }) : null,
1376
+ error ? /* @__PURE__ */ jsx15("p", { role: "alert", className: cn("text-xs text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
1377
+ !loading && !error && documents.length === 0 ? /* @__PURE__ */ jsx15("p", { className: cn("text-xs text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
1378
+ canArchive && documents.length > 0 ? /* @__PURE__ */ jsxs12("div", { className: cn("mb-2 flex flex-wrap items-center gap-3 text-xs", classNames?.selectionBar), children: [
1379
+ /* @__PURE__ */ jsxs12("label", { className: "inline-flex items-center gap-1.5", children: [
1380
+ /* @__PURE__ */ jsx15(
1381
+ "input",
1382
+ {
1383
+ type: "checkbox",
1384
+ checked: allOnPageSelected,
1385
+ onChange: toggleAllOnPage,
1386
+ className: cn(classNames?.checkbox)
1387
+ }
1388
+ ),
1389
+ labels.selectAll
1390
+ ] }),
1391
+ selectedIds.length > 0 ? /* @__PURE__ */ jsx15("button", { type: "button", onClick: () => void handleDownloadSelected(), className: "cv-header-action", children: labels.downloadSelected(selectedIds.length) }) : null,
1392
+ archiveError ? /* @__PURE__ */ jsx15("span", { role: "alert", className: "text-red-600 dark:text-red-400", children: labels.archiveFailed }) : null
1393
+ ] }) : null,
1394
+ /* @__PURE__ */ jsx15("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
1395
+ const isFromCustomer = !TEAM_SOURCES.has(document2.source);
1396
+ const SourceIcon = isFromCustomer ? Users : Bot;
1397
+ return /* @__PURE__ */ jsxs12(
1398
+ "li",
1399
+ {
1400
+ className: cn(
1401
+ "flex items-center justify-between gap-2 rounded-lg border px-3 py-2 dark:border-gray-700",
1402
+ classNames?.item
1403
+ ),
1404
+ children: [
1405
+ /* @__PURE__ */ jsxs12("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
1406
+ canArchive ? /* @__PURE__ */ jsx15(
1407
+ "input",
1408
+ {
1409
+ type: "checkbox",
1410
+ checked: selectedIds.includes(document2.id),
1411
+ onChange: () => toggleSelected(document2.id),
1412
+ "aria-label": `${labels.download}: ${document2.filename}`,
1413
+ className: cn("shrink-0", classNames?.checkbox)
1414
+ }
1415
+ ) : null,
1416
+ /* @__PURE__ */ jsx15(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
1417
+ /* @__PURE__ */ jsxs12("div", { className: "min-w-0 flex-1", children: [
1418
+ /* @__PURE__ */ jsxs12(
1419
+ "div",
1420
+ {
1421
+ className: cn(
1422
+ "mb-0.5 flex items-center gap-1 text-[11px] font-medium",
1423
+ isFromCustomer ? "text-blue-600 dark:text-blue-400" : "text-emerald-600 dark:text-emerald-400",
1424
+ classNames?.sourceBadge
1425
+ ),
1426
+ children: [
1427
+ /* @__PURE__ */ jsx15(SourceIcon, { size: 11 }),
1428
+ isFromCustomer ? labels.sourceFilterCustomer : labels.sourceFilterTeam
1429
+ ]
1430
+ }
1431
+ ),
1432
+ /* @__PURE__ */ jsx15(
1433
+ "div",
1434
+ {
1435
+ className: cn("truncate text-sm font-medium", classNames?.filename),
1436
+ title: document2.filename,
1437
+ children: document2.filename
1438
+ }
1439
+ ),
1440
+ /* @__PURE__ */ jsxs12("div", { className: cn("text-xs text-gray-500 dark:text-gray-400", classNames?.meta), children: [
1441
+ formatDateTime(document2.linkedAt),
1442
+ " \xB7 ",
1443
+ formatFileSize(document2.sizeBytes)
1444
+ ] })
1445
+ ] })
1446
+ ] }),
1447
+ /* @__PURE__ */ jsxs12("div", { className: "flex shrink-0 gap-1", children: [
1448
+ /* @__PURE__ */ jsx15(
1449
+ "button",
1450
+ {
1451
+ type: "button",
1452
+ onClick: () => void handleOpen(document2.id, "inline"),
1453
+ title: labels.view,
1454
+ "aria-label": `${labels.view}: ${document2.filename}`,
1455
+ className: cn("cv-header-icon", classNames?.viewButton),
1456
+ children: /* @__PURE__ */ jsx15(Eye, { size: 14 })
1457
+ }
1458
+ ),
1459
+ /* @__PURE__ */ jsx15(
1460
+ "button",
1461
+ {
1462
+ type: "button",
1463
+ onClick: () => void handleOpen(document2.id, "attachment"),
1464
+ title: labels.download,
1465
+ "aria-label": `${labels.download}: ${document2.filename}`,
1466
+ className: cn("cv-header-icon", classNames?.downloadButton),
1467
+ children: /* @__PURE__ */ jsx15(Download, { size: 14 })
1468
+ }
1469
+ )
1470
+ ] })
1471
+ ]
1472
+ },
1473
+ document2.id
1474
+ );
1475
+ }) }),
1476
+ total > perPage ? /* @__PURE__ */ jsxs12(
1477
+ "div",
1238
1478
  {
1239
- type: "search",
1240
- value: search,
1241
- onChange: (event) => setSearch(event.target.value),
1242
- placeholder: labels.searchPlaceholder,
1243
- className: "mb-2 w-full rounded-md border px-3 py-2 text-sm"
1479
+ className: cn(
1480
+ "mt-2 flex items-center justify-between border-t pt-2 text-xs dark:border-gray-700",
1481
+ classNames?.pagination
1482
+ ),
1483
+ children: [
1484
+ /* @__PURE__ */ jsx15("span", { className: "text-gray-400", children: labels.total(total) }),
1485
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
1486
+ /* @__PURE__ */ jsx15(
1487
+ "button",
1488
+ {
1489
+ type: "button",
1490
+ onClick: () => setPage(page - 1),
1491
+ disabled: page <= 1,
1492
+ "aria-label": labels.sortOldest,
1493
+ className: "cv-header-icon disabled:opacity-40",
1494
+ children: "\u2039"
1495
+ }
1496
+ ),
1497
+ /* @__PURE__ */ jsx15("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
1498
+ /* @__PURE__ */ jsx15(
1499
+ "button",
1500
+ {
1501
+ type: "button",
1502
+ onClick: () => setPage(page + 1),
1503
+ disabled: page >= lastPage,
1504
+ "aria-label": labels.sortMostRecent,
1505
+ className: "cv-header-icon disabled:opacity-40",
1506
+ children: "\u203A"
1507
+ }
1508
+ )
1509
+ ] })
1510
+ ]
1244
1511
  }
1245
- ),
1246
- loading ? /* @__PURE__ */ jsx15("p", { className: "text-xs text-gray-500", children: labels.loading }) : null,
1247
- error ? /* @__PURE__ */ jsx15("p", { className: "text-xs text-red-600 dark:text-red-400", children: labels.failure }) : null,
1248
- !loading && !error && documents.length === 0 ? /* @__PURE__ */ jsx15("p", { className: "text-xs text-gray-500", children: labels.empty }) : null,
1249
- /* @__PURE__ */ jsx15("ul", { className: "space-y-1", children: documents.map((document2) => /* @__PURE__ */ jsxs12("li", { className: "flex items-center gap-2 text-xs", children: [
1250
- /* @__PURE__ */ jsx15(FileIcon, { mimeType: document2.mimeType }),
1251
- /* @__PURE__ */ jsx15("span", { className: "min-w-0 flex-1 truncate", children: document2.filename }),
1252
- /* @__PURE__ */ jsx15("span", { className: "text-gray-500", children: formatFileSize(document2.sizeBytes) }),
1253
- /* @__PURE__ */ jsx15("span", { className: "text-gray-500", children: formatTimestamp(document2.linkedAt) }),
1254
- /* @__PURE__ */ jsx15("button", { type: "button", onClick: () => void handleDownload(document2.id), className: "cv-header-action", children: labels.download })
1255
- ] }, document2.id)) })
1512
+ ) : null
1256
1513
  ] }) });
1257
1514
  }
1258
1515
 
@@ -1293,52 +1550,66 @@ function downloadTextFile(filename, content) {
1293
1550
 
1294
1551
  // src/useWaitingNotifications.ts
1295
1552
  import { useState as useState9, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback4 } from "react";
1296
- function useWaitingNotifications() {
1297
- const conversationsCtx = useConversations();
1553
+ var DEFAULT_POLL_INTERVAL_MS = 1e4;
1554
+ var DEFAULT_LIMIT = 50;
1555
+ var DEFAULT_LABELS = {
1556
+ title: (conversation) => conversation.clientName ?? conversation.whatsappNumber,
1557
+ body: (conversation) => conversation.lastContent ?? "Nova mensagem"
1558
+ };
1559
+ function useWaitingNotifications(params) {
1560
+ const conversationsContext = useConversations();
1298
1561
  const [conversations, setConversations] = useState9([]);
1299
1562
  const previousUnreadMap = useRef4(/* @__PURE__ */ new Map());
1300
1563
  const notifiedIds = useRef4(/* @__PURE__ */ new Set());
1301
- const pollConversations = useCallback4(async () => {
1302
- if (!conversationsCtx) return;
1564
+ const isPollingRef = useRef4(false);
1565
+ const isEnabled = params?.enabled ?? true;
1566
+ const intervalMs = params?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1567
+ const icon = params?.icon;
1568
+ const listParams = params?.params;
1569
+ const labelTitle = params?.labels?.title ?? DEFAULT_LABELS.title;
1570
+ const labelBody = params?.labels?.body ?? DEFAULT_LABELS.body;
1571
+ const refresh = useCallback4(async () => {
1572
+ if (!conversationsContext || isPollingRef.current) return;
1573
+ isPollingRef.current = true;
1303
1574
  try {
1304
- const result = await conversationsCtx.api.fetchConversations({ limit: 50 });
1575
+ const result = conversationsOf(
1576
+ await conversationsContext.api.fetchConversations({ limit: DEFAULT_LIMIT, ...listParams })
1577
+ );
1305
1578
  setConversations(result);
1306
1579
  const currentMap = /* @__PURE__ */ new Map();
1307
- for (const conv of result) {
1308
- currentMap.set(conv.id, conv.unread);
1309
- }
1310
- for (const conv of result) {
1311
- if (conv.unread <= 0) continue;
1312
- const prev = previousUnreadMap.current.get(conv.id) ?? 0;
1313
- if (conv.unread > prev && !notifiedIds.current.has(conv.id)) {
1314
- notifiedIds.current.add(conv.id);
1315
- if (typeof window !== "undefined" && "Notification" in window && Notification.permission === "granted") {
1316
- const title = conv.clientName ?? conv.whatsappNumber;
1317
- const body = conv.lastContent ?? "Nova mensagem";
1318
- new Notification(title, { body, icon: "/favicon.ico" });
1319
- }
1580
+ for (const conversation of result) currentMap.set(conversation.id, conversation.unread);
1581
+ for (const conversation of result) {
1582
+ if (conversation.unread <= 0) continue;
1583
+ const previousUnread = previousUnreadMap.current.get(conversation.id) ?? 0;
1584
+ if (conversation.unread <= previousUnread || notifiedIds.current.has(conversation.id)) continue;
1585
+ notifiedIds.current.add(conversation.id);
1586
+ if (typeof window !== "undefined" && "Notification" in window && Notification.permission === "granted") {
1587
+ new Notification(labelTitle(conversation), { body: labelBody(conversation), ...icon ? { icon } : {} });
1320
1588
  }
1321
1589
  }
1322
1590
  previousUnreadMap.current = currentMap;
1323
1591
  } catch {
1592
+ } finally {
1593
+ isPollingRef.current = false;
1324
1594
  }
1325
- }, [conversationsCtx]);
1595
+ }, [conversationsContext, listParams, icon, labelTitle, labelBody]);
1326
1596
  useEffect5(() => {
1597
+ if (!isEnabled) return;
1327
1598
  if (typeof window !== "undefined" && "Notification" in window && Notification.permission === "default") {
1328
1599
  Notification.requestPermission();
1329
1600
  }
1330
- pollConversations();
1331
- const interval = setInterval(pollConversations, 1e4);
1601
+ void refresh();
1602
+ const interval = setInterval(() => void refresh(), intervalMs);
1332
1603
  return () => clearInterval(interval);
1333
- }, [pollConversations]);
1334
- const unreadCount = conversations.reduce((sum, conv) => sum + conv.unread, 0);
1335
- return { unreadCount, conversations };
1604
+ }, [refresh, isEnabled, intervalMs]);
1605
+ const unreadCount = conversations.reduce((sum, conversation) => sum + conversation.unread, 0);
1606
+ return { unreadCount, conversations, refresh };
1336
1607
  }
1337
1608
 
1338
1609
  // src/settings/WhatsAppTemplateSettingsForm.tsx
1339
1610
  import { Plus, RefreshCw, Save, Trash2 } from "lucide-react";
1340
1611
  import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1341
- var DEFAULT_LABELS = {
1612
+ var DEFAULT_LABELS2 = {
1342
1613
  sectionTitle: "WhatsApp",
1343
1614
  sectionDescription: "Template usado para reabrir a janela de 24h com o cliente.",
1344
1615
  templateLabel: "Template aprovado",
@@ -1372,7 +1643,7 @@ function WhatsAppTemplateSettingsForm({
1372
1643
  onSave,
1373
1644
  labels: labelsOverride
1374
1645
  }) {
1375
- const labels = { ...DEFAULT_LABELS, ...labelsOverride };
1646
+ const labels = { ...DEFAULT_LABELS2, ...labelsOverride };
1376
1647
  const selected = templates.find((template) => template.name === selectedTemplateName);
1377
1648
  function handleSelect(name) {
1378
1649
  const template = templates.find((t) => t.name === name);
@@ -1534,7 +1805,7 @@ function WhatsAppTemplateSettingsForm({
1534
1805
  // src/settings/WhatsAppCreateTemplateForm.tsx
1535
1806
  import { SendHorizonal } from "lucide-react";
1536
1807
  import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1537
- var DEFAULT_LABELS2 = {
1808
+ var DEFAULT_LABELS3 = {
1538
1809
  sectionTitle: "Criar novo template",
1539
1810
  sectionDescription: "Envia um template para aprova\xE7\xE3o da Meta.",
1540
1811
  nameLabel: "Nome do template",
@@ -1584,7 +1855,7 @@ function WhatsAppCreateTemplateForm({
1584
1855
  labels: labelsOverride,
1585
1856
  variableExamples = DEFAULT_VARIABLE_EXAMPLES
1586
1857
  }) {
1587
- const labels = { ...DEFAULT_LABELS2, ...labelsOverride };
1858
+ const labels = { ...DEFAULT_LABELS3, ...labelsOverride };
1588
1859
  const detectedVariables = detectVariables(value.bodyText);
1589
1860
  function set(key, next) {
1590
1861
  onChange({ ...value, [key]: next });
@@ -1770,7 +2041,7 @@ function WhatsAppCreateTemplateForm({
1770
2041
  // src/settings/WelcomeFarewellForm.tsx
1771
2042
  import { LogOut, Mail, Save as Save2 } from "lucide-react";
1772
2043
  import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1773
- var DEFAULT_LABELS3 = {
2044
+ var DEFAULT_LABELS4 = {
1774
2045
  sectionTitle: "Boas-vindas e encerramento",
1775
2046
  welcomeMessage: "Mensagem de boas-vindas",
1776
2047
  welcomeMessagePlaceholder: "Ol\xE1! Sou o assistente virtual da {empresa}...",
@@ -1796,7 +2067,7 @@ function WelcomeFarewellForm({
1796
2067
  farewellPlaceholders = DEFAULT_FAREWELL_PLACEHOLDERS,
1797
2068
  labels: labelsOverride
1798
2069
  }) {
1799
- const labels = { ...DEFAULT_LABELS3, ...labelsOverride };
2070
+ const labels = { ...DEFAULT_LABELS4, ...labelsOverride };
1800
2071
  return /* @__PURE__ */ jsxs15("section", { className: "bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden", children: [
1801
2072
  /* @__PURE__ */ jsx18("div", { className: "px-6 py-4 border-b border-gray-100 dark:border-gray-700", children: /* @__PURE__ */ jsx18("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.sectionTitle }) }),
1802
2073
  /* @__PURE__ */ jsxs15("form", { onSubmit: onSave, className: "p-6 space-y-5", children: [
@@ -1899,7 +2170,7 @@ function WhatsAppTemplatesSettings({
1899
2170
  // src/settings/TopicsForm.tsx
1900
2171
  import { Megaphone, Save as Save3 } from "lucide-react";
1901
2172
  import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
1902
- var DEFAULT_LABELS4 = {
2173
+ var DEFAULT_LABELS5 = {
1903
2174
  sectionTitle: "T\xF3picos intermedi\xE1rios",
1904
2175
  sectionDescription: "Mensagens autom\xE1ticas para assuntos espec\xEDficos, disparadas quando o cliente demonstra interesse.",
1905
2176
  messagePlaceholder: "Digite a mensagem autom\xE1tica para este t\xF3pico...",
@@ -1915,7 +2186,7 @@ function TopicsForm({
1915
2186
  saveSuccess = false,
1916
2187
  labels: labelsOverride
1917
2188
  }) {
1918
- const labels = { ...DEFAULT_LABELS4, ...labelsOverride };
2189
+ const labels = { ...DEFAULT_LABELS5, ...labelsOverride };
1919
2190
  return /* @__PURE__ */ jsxs17("section", { className: "bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden", children: [
1920
2191
  /* @__PURE__ */ jsxs17("div", { className: "px-6 py-4 border-b border-gray-100 dark:border-gray-700", children: [
1921
2192
  /* @__PURE__ */ jsxs17("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2", children: [
@@ -2022,11 +2293,15 @@ function useConversationList(params) {
2022
2293
  throw new Error("useConversationList requires an ancestor <ConversationsProvider>");
2023
2294
  }
2024
2295
  const { api } = context;
2296
+ const filtersKey = JSON.stringify(params?.filters ?? {});
2025
2297
  const { data, loading, error, refetch } = useAsyncResource(
2026
2298
  () => api.fetchConversations(params),
2027
- [params?.page, params?.limit, params?.waitingHuman, params?.search]
2299
+ [params?.page, params?.limit, params?.waitingHuman, params?.search, filtersKey]
2028
2300
  );
2029
- return { conversations: data ?? [], loading, error, refetch };
2301
+ if (data === void 0) {
2302
+ return { conversations: [], total: 0, loading, error, refetch };
2303
+ }
2304
+ return { conversations: conversationsOf(data), total: totalOf(data), loading, error, refetch };
2030
2305
  }
2031
2306
 
2032
2307
  // src/hooks/useConversationContext.ts
@@ -2072,6 +2347,38 @@ function useGlobalRealtime(onEvent) {
2072
2347
  };
2073
2348
  }, [context]);
2074
2349
  }
2350
+
2351
+ // src/hooks/useConversationActions.ts
2352
+ import { useMemo as useMemo2 } from "react";
2353
+ function useConversationActions(conversationId) {
2354
+ const context = useConversations();
2355
+ if (!context) {
2356
+ throw new Error("useConversationActions requires an ancestor <ConversationsProvider>");
2357
+ }
2358
+ const { api } = context;
2359
+ return useMemo2(
2360
+ () => ({
2361
+ takeover: api.takeover ? () => api.takeover(conversationId) : void 0,
2362
+ release: api.release ? () => api.release(conversationId) : void 0,
2363
+ finalize: api.finalize ? () => api.finalize(conversationId) : void 0
2364
+ }),
2365
+ [api, conversationId]
2366
+ );
2367
+ }
2368
+ function useInboxActions() {
2369
+ const context = useConversations();
2370
+ if (!context) {
2371
+ throw new Error("useInboxActions requires an ancestor <ConversationsProvider>");
2372
+ }
2373
+ const { api } = context;
2374
+ return useMemo2(
2375
+ () => ({
2376
+ markAllRead: api.markAllRead ? () => api.markAllRead() : void 0,
2377
+ listTemplates: api.listTemplates ? () => api.listTemplates() : void 0
2378
+ }),
2379
+ [api]
2380
+ );
2381
+ }
2075
2382
  export {
2076
2383
  AudioPlayer,
2077
2384
  Avatar,
@@ -2127,15 +2434,18 @@ export {
2127
2434
  contactFlag,
2128
2435
  downloadTextFile,
2129
2436
  formatContactHandle,
2437
+ formatDateTime,
2130
2438
  formatFileSize,
2131
2439
  formatPhone,
2132
2440
  formatStalledFor,
2133
2441
  formatTimestamp,
2134
2442
  htmlToWA,
2443
+ isSameDay,
2135
2444
  isWindowBlocking,
2136
2445
  parseWhatsAppFormatting,
2137
2446
  phoneInitials,
2138
2447
  toast,
2448
+ useConversationActions,
2139
2449
  useConversationContext,
2140
2450
  useConversationDocuments,
2141
2451
  useConversationList,
@@ -2145,6 +2455,7 @@ export {
2145
2455
  useConversations,
2146
2456
  useDarkMode,
2147
2457
  useGlobalRealtime,
2458
+ useInboxActions,
2148
2459
  useIsDarkTheme,
2149
2460
  useIsNarrow,
2150
2461
  useToast,
@@ -1,4 +1,4 @@
1
- import { M as MessagePayload, i as ConversationSummary, h as ConversationEventSource, j as ConversationsApi, S as SSEProvider } from '../types-C0PtaO7S.js';
1
+ import { M as MessagePayload, k as ConversationSummary, i as ConversationEventSource, m as ConversationsApi, L as ListConversationsParams, j as ConversationPage, S as SSEProvider, g as ConversationDocument } from '../types-C2Yexi8A.js';
2
2
  import * as react from 'react';
3
3
  import { InteractiveReplyOption } from '@adatechnology/meta-whatsapp-contracts/testing';
4
4
 
@@ -81,7 +81,15 @@ type CreateMockConversationsApiParams = {
81
81
  readonly latencyMs?: number;
82
82
  readonly agentName?: string;
83
83
  };
84
- declare function createMockConversationsApi(params: CreateMockConversationsApiParams): ConversationsApi;
84
+ /**
85
+ * O mock satisfaz `ConversationsApi`, mas com o retorno de `fetchConversations` ESTREITADO para a
86
+ * forma paginada. Sem isto o contrato — que aceita array ou página — obrigaria todo consumidor do
87
+ * preview a desempacotar uma união que aqui nunca varia.
88
+ */
89
+ type MockConversationsApi = Omit<ConversationsApi, 'fetchConversations'> & {
90
+ fetchConversations(params?: ListConversationsParams): Promise<ConversationPage>;
91
+ };
92
+ declare function createMockConversationsApi(params: CreateMockConversationsApiParams): MockConversationsApi;
85
93
 
86
94
  /**
87
95
  * `SSEProvider` servido pelo store em memória. Mesmo mapeamento de canal do servidor
@@ -101,6 +109,12 @@ declare function createMockSSEProvider(params: CreateMockSSEProviderParams): SSE
101
109
 
102
110
  declare const PREVIEW_CONVERSATIONS: readonly ConversationSummary[];
103
111
  declare const PREVIEW_MESSAGES: Readonly<Record<string, readonly MessagePayload[]>>;
112
+ /**
113
+ * A biblioteca de arquivos da conversa, como o backend a devolveria. Deriva das mensagens de
114
+ * documento acima em vez de repetir os dados: fixture duplicada divergiria na primeira edição, e o
115
+ * painel passaria a mostrar arquivo que a thread não tem.
116
+ */
117
+ declare const PREVIEW_DOCUMENTS: Readonly<Record<string, readonly ConversationDocument[]>>;
104
118
 
105
119
  /**
106
120
  * Cliente que entrega mensagens do preview no webhook real, assinadas com HMAC — a mesma validação
@@ -169,4 +183,4 @@ type StartPreviewScriptParams = {
169
183
  };
170
184
  declare function startPreviewScript(params: StartPreviewScriptParams): () => void;
171
185
 
172
- export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_PREVIEW_SCRIPT, GLOBAL_CHANNEL, type ListConversationsFilters, type MockEventSource, PREVIEW_CONVERSATIONS, PREVIEW_MESSAGES, type PreviewEmission, PreviewInProductionError, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewWebhookClient, PreviewWebhookRejectedError, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewStore, createPreviewWebhookClient, startPreviewScript };
186
+ export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_PREVIEW_SCRIPT, GLOBAL_CHANNEL, type ListConversationsFilters, type MockEventSource, PREVIEW_CONVERSATIONS, PREVIEW_DOCUMENTS, PREVIEW_MESSAGES, type PreviewEmission, PreviewInProductionError, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewWebhookClient, PreviewWebhookRejectedError, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewStore, createPreviewWebhookClient, startPreviewScript };