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

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.
Files changed (53) hide show
  1. package/dist/chunk-4R6Y43DQ.js +726 -0
  2. package/dist/chunk-NV2RZ5KT.js +56 -0
  3. package/dist/{chunk-ZDURDZTM.js → chunk-OGRRHQQW.js} +1 -41
  4. package/dist/flows/index.js +6 -4
  5. package/dist/index.d.ts +323 -111
  6. package/dist/index.js +1032 -954
  7. package/dist/preview/index.d.ts +172 -0
  8. package/dist/preview/index.js +576 -0
  9. package/dist/styles.css +198 -0
  10. package/dist/types-C0PtaO7S.d.ts +207 -0
  11. package/package.json +10 -3
  12. package/src/Avatar.tsx +18 -3
  13. package/src/ChannelIcon.tsx +87 -0
  14. package/src/ConversationContextPanel.tsx +106 -0
  15. package/src/ConversationDocumentsPanel.tsx +107 -0
  16. package/src/ConversationHeader.tsx +239 -0
  17. package/src/ConversationListItem.tsx +36 -5
  18. package/src/ConversationLocalesProvider.tsx +16 -0
  19. package/src/ConversationRow.tsx +137 -0
  20. package/src/DateDivider.tsx +16 -3
  21. package/src/MediaRenderer.tsx +9 -9
  22. package/src/MessageBubble.tsx +24 -2
  23. package/src/MessageComposer.tsx +15 -2
  24. package/src/Wallpaper.tsx +4 -2
  25. package/src/WindowExpiredNotice.tsx +57 -0
  26. package/src/conversationChannel.test.ts +53 -0
  27. package/src/conversationChannel.ts +146 -0
  28. package/src/conversationTranscript.test.ts +65 -0
  29. package/src/conversationTranscript.ts +64 -0
  30. package/src/conversationWindow.test.ts +90 -0
  31. package/src/conversationWindow.ts +78 -0
  32. package/src/flows/FlowMapCanvas.tsx +2 -2
  33. package/src/hooks/useConversationDocuments.ts +4 -2
  34. package/src/index.ts +73 -4
  35. package/src/lib/cn.ts +15 -0
  36. package/src/lib/phone.ts +34 -0
  37. package/src/preview/ConversationPreview.tsx +148 -0
  38. package/src/preview/createMockConversationsApi.ts +111 -0
  39. package/src/preview/createMockSSEProvider.ts +40 -0
  40. package/src/preview/createPreviewWebhookClient.test.ts +105 -0
  41. package/src/preview/createPreviewWebhookClient.ts +99 -0
  42. package/src/preview/index.ts +40 -0
  43. package/src/preview/mockEventSource.ts +53 -0
  44. package/src/preview/preview.test.ts +175 -0
  45. package/src/preview/previewFixtures.ts +153 -0
  46. package/src/preview/previewStore.ts +193 -0
  47. package/src/preview/startPreviewScript.ts +60 -0
  48. package/src/providers/types.ts +36 -2
  49. package/src/settings/WhatsAppTemplatesSettings.tsx +106 -0
  50. package/src/styles.css +136 -0
  51. package/src/types.ts +8 -0
  52. package/src/useDarkMode.ts +26 -0
  53. package/src/useIsNarrow.ts +29 -0
@@ -0,0 +1,726 @@
1
+ import {
2
+ parseWhatsAppFormatting
3
+ } from "./chunk-OGRRHQQW.js";
4
+
5
+ // src/ConversationLocalesProvider.tsx
6
+ import { createContext, useContext } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var DEFAULT_LOCALES = {
9
+ bubble: {
10
+ customer: "Cliente",
11
+ bot: "Bot",
12
+ agent: "Atendente",
13
+ document: "documento",
14
+ media: "m\xEDdia",
15
+ templateLabel: "Template",
16
+ readAt: "Lida \xE0s ",
17
+ windowExpired: "Janela de 24h expirada",
18
+ viewImage: "Ver imagem",
19
+ listenAudio: "Ouvir \xE1udio",
20
+ viewVideo: "Ver v\xEDdeo",
21
+ moderationFlagged: "Linguagem ofensiva",
22
+ mediaLoading: "Carregando...",
23
+ mediaRetry: "Erro \u2014 tentar novamente",
24
+ mediaError: "Erro",
25
+ mediaUnavailable: "M\xEDdia indispon\xEDvel",
26
+ imageAlt: "Imagem",
27
+ untitledDocument: "Documento",
28
+ downloadFile: "Baixar"
29
+ },
30
+ selection: {
31
+ select: "Selecionar"
32
+ },
33
+ dateDivider: {
34
+ today: "Hoje",
35
+ yesterday: "Ontem"
36
+ }
37
+ };
38
+ var ConversationLocalesContext = createContext(DEFAULT_LOCALES);
39
+ function ConversationLocalesProvider({ children, locales }) {
40
+ const merged = {
41
+ bubble: { ...DEFAULT_LOCALES.bubble, ...locales?.bubble },
42
+ selection: { ...DEFAULT_LOCALES.selection, ...locales?.selection },
43
+ dateDivider: { ...DEFAULT_LOCALES.dateDivider, ...locales?.dateDivider }
44
+ };
45
+ return /* @__PURE__ */ jsx(ConversationLocalesContext.Provider, { value: merged, children });
46
+ }
47
+ function useConversationLocales() {
48
+ return useContext(ConversationLocalesContext);
49
+ }
50
+
51
+ // src/StatusTicks.tsx
52
+ import { AlertTriangle } from "lucide-react";
53
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
54
+ var STATUS_COLOR_CLASS = {
55
+ sent: "text-black/40 dark:text-white/40",
56
+ delivered: "text-black/40 dark:text-white/40",
57
+ read: "text-sky-500",
58
+ failed: "text-red-500"
59
+ };
60
+ function Ticks({ double }) {
61
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 12", width: "15", height: "9", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
62
+ double && /* @__PURE__ */ jsx2("path", { d: "M1 6.5L4.5 10L11 2", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }),
63
+ /* @__PURE__ */ jsx2(
64
+ "path",
65
+ {
66
+ d: double ? "M6 6.5L9.5 10L19 1" : "M1 6.5L5 10.5L14.5 1",
67
+ stroke: "currentColor",
68
+ strokeWidth: "1.6",
69
+ strokeLinecap: "round",
70
+ strokeLinejoin: "round"
71
+ }
72
+ )
73
+ ] });
74
+ }
75
+ function StatusTicks({ status, title }) {
76
+ const colorClass = STATUS_COLOR_CLASS[status] ?? STATUS_COLOR_CLASS.sent;
77
+ return /* @__PURE__ */ jsx2("span", { className: `cursor-help leading-none flex items-center ${colorClass}`, title, children: status === "failed" ? /* @__PURE__ */ jsx2(AlertTriangle, { size: 11 }) : /* @__PURE__ */ jsx2(Ticks, { double: status !== "sent" }) });
78
+ }
79
+
80
+ // src/AudioPlayer.tsx
81
+ import { useEffect, useRef, useState } from "react";
82
+ import { Pause, Play } from "lucide-react";
83
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
84
+ var BARS = Array.from({ length: 30 }, (_, i) => {
85
+ const heights = [3, 5, 8, 12, 7, 10, 14, 9, 6, 11, 15, 8, 4, 13, 7, 10, 6, 12, 9, 5, 14, 8, 11, 6, 13, 7, 10, 5, 9, 4];
86
+ return heights[i % heights.length];
87
+ });
88
+ function fmt(sec) {
89
+ if (!isFinite(sec)) return "0:00";
90
+ const m = Math.floor(sec / 60);
91
+ const s = Math.floor(sec % 60);
92
+ return `${m}:${s.toString().padStart(2, "0")}`;
93
+ }
94
+ function AudioPlayer({ src, isMine = false }) {
95
+ const audioRef = useRef(null);
96
+ const [playing, setPlaying] = useState(false);
97
+ const [progress, setProgress] = useState(0);
98
+ const [duration, setDuration] = useState(0);
99
+ const [current, setCurrent] = useState(0);
100
+ useEffect(() => {
101
+ const audio = audioRef.current;
102
+ if (!audio) return;
103
+ const onTime = () => {
104
+ setCurrent(audio.currentTime);
105
+ setProgress(audio.duration ? audio.currentTime / audio.duration : 0);
106
+ };
107
+ const onMeta = () => setDuration(audio.duration);
108
+ const onEnd = () => {
109
+ setPlaying(false);
110
+ setProgress(0);
111
+ setCurrent(0);
112
+ };
113
+ audio.addEventListener("timeupdate", onTime);
114
+ audio.addEventListener("loadedmetadata", onMeta);
115
+ audio.addEventListener("ended", onEnd);
116
+ return () => {
117
+ audio.removeEventListener("timeupdate", onTime);
118
+ audio.removeEventListener("loadedmetadata", onMeta);
119
+ audio.removeEventListener("ended", onEnd);
120
+ };
121
+ }, []);
122
+ const toggle = () => {
123
+ const audio = audioRef.current;
124
+ if (!audio) return;
125
+ if (playing) {
126
+ audio.pause();
127
+ setPlaying(false);
128
+ } else {
129
+ audio.play();
130
+ setPlaying(true);
131
+ }
132
+ };
133
+ const seek = (e) => {
134
+ const audio = audioRef.current;
135
+ if (!audio || !audio.duration) return;
136
+ const rect = e.currentTarget.getBoundingClientRect();
137
+ const ratio = (e.clientX - rect.left) / rect.width;
138
+ audio.currentTime = ratio * audio.duration;
139
+ };
140
+ const activeBars = Math.round(progress * BARS.length);
141
+ const playBtn = isMine ? "bg-green-600 hover:bg-green-700 text-white" : "bg-gray-600 hover:bg-gray-700 text-white";
142
+ const activeBar = isMine ? "bg-green-700" : "bg-gray-700";
143
+ const inactiveBar = isMine ? "bg-green-300" : "bg-gray-300";
144
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", style: { minWidth: "200px", maxWidth: "260px" }, children: [
145
+ /* @__PURE__ */ jsx3("audio", { ref: audioRef, src, preload: "metadata" }),
146
+ /* @__PURE__ */ jsx3(
147
+ "button",
148
+ {
149
+ onClick: toggle,
150
+ className: `flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center transition-colors ${playBtn}`,
151
+ children: playing ? /* @__PURE__ */ jsx3(Pause, { size: 16 }) : /* @__PURE__ */ jsx3(Play, { size: 16, className: "translate-x-0.5" })
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-1", children: [
155
+ /* @__PURE__ */ jsx3("div", { className: "flex items-end gap-0.5 h-8 cursor-pointer", onClick: seek, children: BARS.map((h, i) => /* @__PURE__ */ jsx3(
156
+ "div",
157
+ {
158
+ className: `flex-1 rounded-full transition-colors ${i < activeBars ? activeBar : inactiveBar}`,
159
+ style: { height: `${h * 2}px` }
160
+ },
161
+ i
162
+ )) }),
163
+ /* @__PURE__ */ jsx3("span", { className: "text-gray-400 tabular-nums text-xs", children: playing || current > 0 ? fmt(current) : fmt(duration) })
164
+ ] })
165
+ ] });
166
+ }
167
+
168
+ // src/FileIcon.tsx
169
+ import { FileArchive, FileSpreadsheet, FileText, File as FileGeneric } from "lucide-react";
170
+ import { jsx as jsx4 } from "react/jsx-runtime";
171
+ var EXTENSION_STYLE = {
172
+ pdf: { Icon: FileText, colorClass: "text-red-500" },
173
+ doc: { Icon: FileText, colorClass: "text-blue-500" },
174
+ docx: { Icon: FileText, colorClass: "text-blue-500" },
175
+ xls: { Icon: FileSpreadsheet, colorClass: "text-green-600" },
176
+ xlsx: { Icon: FileSpreadsheet, colorClass: "text-green-600" },
177
+ zip: { Icon: FileArchive, colorClass: "text-orange-500" }
178
+ };
179
+ function getExtension(filename, mimeType) {
180
+ const fromFilename = filename?.split(".").pop()?.toLowerCase();
181
+ if (fromFilename && EXTENSION_STYLE[fromFilename]) return fromFilename;
182
+ return mimeType?.split("/")[1]?.toLowerCase() ?? "";
183
+ }
184
+ function FileIcon({ filename, mimeType, size = 20, className = "" }) {
185
+ const extension = getExtension(filename, mimeType);
186
+ const style = EXTENSION_STYLE[extension] ?? { Icon: FileGeneric, colorClass: "text-gray-500" };
187
+ const { Icon, colorClass } = style;
188
+ return /* @__PURE__ */ jsx4(Icon, { size, className: `${colorClass} ${className}`.trim() });
189
+ }
190
+
191
+ // src/lib/format.ts
192
+ function formatTimestamp(timestamp) {
193
+ try {
194
+ const date = new Date(timestamp);
195
+ const hours = date.getHours().toString().padStart(2, "0");
196
+ const minutes = date.getMinutes().toString().padStart(2, "0");
197
+ return `${hours}:${minutes}`;
198
+ } catch {
199
+ return timestamp;
200
+ }
201
+ }
202
+ function formatDateTime(iso) {
203
+ const d = new Date(iso);
204
+ return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
205
+ }
206
+ function isSameDay(a, b) {
207
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
208
+ }
209
+ var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB"];
210
+ function formatFileSize(bytes) {
211
+ if (!isFinite(bytes) || bytes < 0) return "";
212
+ if (bytes < 1) return "0 B";
213
+ const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), FILE_SIZE_UNITS.length - 1);
214
+ const value = bytes / 1024 ** exponent;
215
+ const formatted = exponent === 0 ? value.toString() : value.toFixed(value < 10 ? 1 : 0);
216
+ return `${formatted} ${FILE_SIZE_UNITS[exponent]}`;
217
+ }
218
+
219
+ // src/MediaRenderer.tsx
220
+ import { useState as useState2 } from "react";
221
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
222
+ function resolveMediaSource(message) {
223
+ if (message.mediaUrl) return message.mediaUrl;
224
+ if (message.base64) {
225
+ const prefix = message.mimeType ? `data:${message.mimeType};base64,` : "data:application/octet-stream;base64,";
226
+ return prefix + message.base64;
227
+ }
228
+ return null;
229
+ }
230
+ function hasLazyRef(message) {
231
+ return Boolean(message.uploadId || message.mediaId);
232
+ }
233
+ function useLazyMediaUrl(message, onResolveUrl) {
234
+ const [url, setUrl] = useState2(null);
235
+ const [loading, setLoading] = useState2(false);
236
+ const [error, setError] = useState2(false);
237
+ const load = async () => {
238
+ if (url || loading || !onResolveUrl) return;
239
+ setLoading(true);
240
+ setError(false);
241
+ try {
242
+ const resolved = await onResolveUrl(message);
243
+ if (resolved) setUrl(resolved);
244
+ else setError(true);
245
+ } catch {
246
+ setError(true);
247
+ } finally {
248
+ setLoading(false);
249
+ }
250
+ };
251
+ return { url, loading, error, load };
252
+ }
253
+ function MediaRenderer({ message, onLightbox, onResolveUrl }) {
254
+ const { bubble } = useConversationLocales();
255
+ const eagerSrc = resolveMediaSource(message);
256
+ const lazy = useLazyMediaUrl(message, onResolveUrl);
257
+ const src = eagerSrc ?? lazy.url;
258
+ const canLazyLoad = !eagerSrc && hasLazyRef(message) && Boolean(onResolveUrl);
259
+ const lazyButtonClass = "text-xs text-blue-600 underline flex items-center gap-1";
260
+ switch (message.type) {
261
+ case "image":
262
+ case "sticker": {
263
+ if (!src && canLazyLoad) {
264
+ return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewImage });
265
+ }
266
+ return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5("img", { src, alt: message.caption ?? bubble.imageAlt, className: "w-full max-h-80 object-cover cursor-pointer hover:opacity-90 transition-opacity", onClick: () => onLightbox(src), loading: "lazy" }) : /* @__PURE__ */ jsx5("div", { className: "w-full h-40 bg-gray-200 flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs3("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
267
+ /* @__PURE__ */ jsx5("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
268
+ /* @__PURE__ */ jsx5("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
269
+ /* @__PURE__ */ jsx5("polyline", { points: "21 15 16 10 5 21" })
270
+ ] }) }) });
271
+ }
272
+ case "video": {
273
+ if (!src && canLazyLoad) {
274
+ return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewVideo });
275
+ }
276
+ return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5("video", { src, className: "w-full max-h-80 rounded-lg", controls: true, preload: "metadata", children: /* @__PURE__ */ jsx5("track", { kind: "captions" }) }) : /* @__PURE__ */ jsx5("div", { className: "w-full h-32 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs3("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
277
+ /* @__PURE__ */ jsx5("polygon", { points: "23 7 16 12 23 17 23 7" }),
278
+ /* @__PURE__ */ jsx5("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
279
+ ] }) }) });
280
+ }
281
+ case "audio": {
282
+ if (!src && canLazyLoad) {
283
+ return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.listenAudio });
284
+ }
285
+ return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5(AudioPlayer, { src, isMine: message.direction === "outbound" }) : /* @__PURE__ */ jsx5("div", { className: "h-12 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400 text-xs", children: bubble.mediaUnavailable }) });
286
+ }
287
+ case "document": {
288
+ const typeLabel = message.mimeType?.split("/")[1]?.toUpperCase() ?? "FILE";
289
+ const sizeLabel = message.sizeBytes ? formatFileSize(message.sizeBytes) : null;
290
+ return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 min-w-[200px]", children: [
291
+ /* @__PURE__ */ jsx5("div", { className: "w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx5(FileIcon, { filename: message.filename, mimeType: message.mimeType }) }),
292
+ /* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
293
+ /* @__PURE__ */ jsx5("p", { className: "text-sm font-medium truncate", children: message.filename ?? bubble.untitledDocument }),
294
+ /* @__PURE__ */ jsx5("p", { className: "text-xs text-gray-500", children: sizeLabel ? `${typeLabel} \xB7 ${sizeLabel}` : typeLabel })
295
+ ] }),
296
+ src ? /* @__PURE__ */ jsx5("a", { href: src, download: message.filename, className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors", "aria-label": bubble.downloadFile, children: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
297
+ /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
298
+ /* @__PURE__ */ jsx5("polyline", { points: "7 10 12 15 17 10" }),
299
+ /* @__PURE__ */ jsx5("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
300
+ ] }) }) : canLazyLoad ? /* @__PURE__ */ jsx5(
301
+ "button",
302
+ {
303
+ onClick: lazy.load,
304
+ disabled: lazy.loading,
305
+ className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors disabled:opacity-50",
306
+ "aria-label": bubble.downloadFile,
307
+ children: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
308
+ /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
309
+ /* @__PURE__ */ jsx5("polyline", { points: "7 10 12 15 17 10" }),
310
+ /* @__PURE__ */ jsx5("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
311
+ ] })
312
+ }
313
+ ) : null,
314
+ lazy.error && /* @__PURE__ */ jsx5("span", { className: "text-xs text-red-500 flex-shrink-0", children: bubble.mediaError })
315
+ ] });
316
+ }
317
+ default:
318
+ return null;
319
+ }
320
+ }
321
+
322
+ // src/Lightbox.tsx
323
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
324
+ function Lightbox({ imageUrl, caption, onClose }) {
325
+ return /* @__PURE__ */ jsx6("div", { className: "fixed inset-0 z-50 bg-black/85 flex items-center justify-center p-4", onClick: onClose, children: /* @__PURE__ */ jsxs4("div", { className: "max-w-[90vw] max-h-[90vh] flex flex-col items-center", onClick: (e) => e.stopPropagation(), children: [
326
+ /* @__PURE__ */ jsx6("img", { src: imageUrl, alt: caption ?? "Image", className: "max-w-full max-h-[80vh] object-contain rounded-lg" }),
327
+ caption && /* @__PURE__ */ jsx6("p", { className: "text-white text-sm mt-3 text-center", children: caption }),
328
+ /* @__PURE__ */ jsx6("button", { onClick: onClose, className: "mt-4 px-4 py-2 bg-white/20 text-white rounded-lg hover:bg-white/30 transition-colors", children: "Fechar" })
329
+ ] }) });
330
+ }
331
+
332
+ // src/MessageBubble.tsx
333
+ import { useState as useState3 } from "react";
334
+ import { Check } from "lucide-react";
335
+
336
+ // src/lib/cn.ts
337
+ import { clsx } from "clsx";
338
+ import { twMerge } from "tailwind-merge";
339
+ function cn(...inputs) {
340
+ return twMerge(clsx(inputs));
341
+ }
342
+
343
+ // src/MessageBubble.tsx
344
+ import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
345
+ var BUBBLE_COLOR = {
346
+ agent: "bg-[#d9fdd3] dark:bg-[#005c4b]",
347
+ bot: "bg-[#d7f0ec] dark:bg-[#0a3d3a]",
348
+ customer: "bg-white dark:bg-[#202c33]"
349
+ };
350
+ var MEDIA_TYPES = /* @__PURE__ */ new Set(["image", "audio", "video", "document", "sticker"]);
351
+ function MessageBubble({
352
+ message,
353
+ isMine,
354
+ senderName,
355
+ isFirstInGroup = true,
356
+ isSelecting = false,
357
+ isSelected = false,
358
+ onToggleSelect,
359
+ onResolveMediaUrl,
360
+ className
361
+ }) {
362
+ const { bubble, selection } = useConversationLocales();
363
+ const [lightboxSrc, setLightboxSrc] = useState3(null);
364
+ const bubbleColor = BUBBLE_COLOR[message.sender] ?? BUBBLE_COLOR.customer;
365
+ const hasError = message.status === "failed";
366
+ const isMedia = MEDIA_TYPES.has(message.type);
367
+ const isTemplate = message.type === "template";
368
+ const displayName = message.sender === "agent" && senderName ? senderName : bubble[message.sender] ?? message.sender;
369
+ const tooltipText = message.status === "read" && message.readAt ? `${bubble.readAt}${formatDateTime(message.readAt)}` : message.status === "failed" ? bubble.windowExpired : void 0;
370
+ const tailCornerClass = isFirstInGroup ? isMine ? "rounded-tr-md" : "rounded-tl-md" : "";
371
+ const checkbox = /* @__PURE__ */ jsx7(
372
+ "button",
373
+ {
374
+ onClick: (e) => {
375
+ e.stopPropagation();
376
+ onToggleSelect?.();
377
+ },
378
+ title: selection.select,
379
+ className: `
380
+ flex-shrink-0 self-end mb-1 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all
381
+ ${isSelected ? "bg-teal-600 border-teal-600" : "bg-white/80 dark:bg-black/40 border-black/20 dark:border-white/30"}
382
+ ${isSelecting ? "opacity-100" : "opacity-0 group-hover:opacity-100"}
383
+ `,
384
+ children: isSelected && /* @__PURE__ */ jsx7(Check, { size: 12, className: "text-white", strokeWidth: 3 })
385
+ }
386
+ );
387
+ return /* @__PURE__ */ jsxs5(
388
+ "div",
389
+ {
390
+ className: cn(
391
+ "flex items-end gap-1.5 group",
392
+ isMine ? "justify-end" : "justify-start",
393
+ isFirstInGroup ? "mt-2" : "mt-0.5",
394
+ className
395
+ ),
396
+ children: [
397
+ isMine && checkbox,
398
+ /* @__PURE__ */ jsxs5(
399
+ "div",
400
+ {
401
+ onClick: isSelecting ? onToggleSelect : void 0,
402
+ className: `
403
+ max-w-[75%] sm:max-w-[65%] rounded-2xl ${tailCornerClass} px-2.5 py-1.5 shadow-sm
404
+ ${bubbleColor}
405
+ ${hasError ? "ring-1 ring-inset ring-red-400" : ""}
406
+ ${isSelecting ? "cursor-pointer" : ""}
407
+ ${isSelected ? "ring-2 ring-teal-500" : ""}
408
+ relative
409
+ `,
410
+ children: [
411
+ isMine && isFirstInGroup && (message.sender === "bot" || message.sender === "agent" && senderName) && /* @__PURE__ */ jsx7("div", { className: "text-xs font-semibold mb-0.5 text-teal-700 dark:text-teal-400", children: displayName }),
412
+ message.moderation?.isOffensive && /* @__PURE__ */ jsxs5(
413
+ "div",
414
+ {
415
+ className: "mb-1 inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-950/60 dark:text-amber-300",
416
+ title: message.moderation.terms.length > 0 ? message.moderation.terms.join(", ") : void 0,
417
+ children: [
418
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": true, children: "\u26A0\uFE0F" }),
419
+ /* @__PURE__ */ jsx7("span", { children: bubble.moderationFlagged })
420
+ ]
421
+ }
422
+ ),
423
+ isMedia ? /* @__PURE__ */ jsx7(MediaRenderer, { message, onLightbox: setLightboxSrc, onResolveUrl: onResolveMediaUrl }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
424
+ isTemplate && /* @__PURE__ */ jsxs5("p", { className: "text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mb-0.5", children: [
425
+ /* @__PURE__ */ jsx7("span", { children: "\u{1F4E8}" }),
426
+ /* @__PURE__ */ jsxs5("span", { className: "font-medium", children: [
427
+ bubble.templateLabel,
428
+ message.templateName ? ` \u2014 ${message.templateName}` : ""
429
+ ] })
430
+ ] }),
431
+ /* @__PURE__ */ jsx7("div", { className: "text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]", children: parseWhatsAppFormatting(message.content ?? "") })
432
+ ] }),
433
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-end gap-1 mt-0.5 select-none", children: [
434
+ /* @__PURE__ */ jsx7("span", { className: "text-xs text-black/40 dark:text-white/40 font-medium", children: formatTimestamp(message.timestamp) }),
435
+ isMine && message.status && /* @__PURE__ */ jsx7(StatusTicks, { status: message.status, title: tooltipText })
436
+ ] })
437
+ ]
438
+ }
439
+ ),
440
+ !isMine && checkbox,
441
+ lightboxSrc && /* @__PURE__ */ jsx7(Lightbox, { imageUrl: lightboxSrc, onClose: () => setLightboxSrc(null) })
442
+ ]
443
+ }
444
+ );
445
+ }
446
+
447
+ // src/Wallpaper.tsx
448
+ import { jsx as jsx8 } from "react/jsx-runtime";
449
+ function ConversationWallpaper({ children, className }) {
450
+ return /* @__PURE__ */ jsx8("div", { className: cn("cv-wallpaper", className), children });
451
+ }
452
+
453
+ // src/EmojiPicker.tsx
454
+ import { useState as useState4, useCallback } from "react";
455
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
456
+ var EMOJI_CATEGORIES = [
457
+ {
458
+ name: "Smileys",
459
+ emojis: ["\u{1F600}", "\u{1F603}", "\u{1F604}", "\u{1F601}", "\u{1F605}", "\u{1F602}", "\u{1F923}", "\u{1F60A}", "\u{1F607}", "\u{1F642}", "\u{1F609}", "\u{1F60C}", "\u{1F60D}", "\u{1F970}", "\u{1F618}", "\u{1F617}", "\u{1F60B}", "\u{1F61B}", "\u{1F61C}", "\u{1F92A}"]
460
+ },
461
+ {
462
+ name: "Gestures",
463
+ emojis: ["\u{1F44D}", "\u{1F44E}", "\u{1F44C}", "\u270C\uFE0F", "\u{1F91E}", "\u{1F91F}", "\u{1F918}", "\u{1F919}", "\u{1F44B}", "\u{1F91A}", "\u{1F590}\uFE0F", "\u270B", "\u{1F596}", "\u{1F44F}", "\u{1F64C}", "\u{1F91D}", "\u{1F64F}", "\u270D\uFE0F", "\u{1F485}", "\u{1F933}"]
464
+ },
465
+ {
466
+ name: "Hearts",
467
+ emojis: ["\u2764\uFE0F", "\u{1F9E1}", "\u{1F49B}", "\u{1F49A}", "\u{1F499}", "\u{1F49C}", "\u{1F5A4}", "\u{1F90D}", "\u{1F90E}", "\u{1F494}", "\u2763\uFE0F", "\u{1F495}", "\u{1F49E}", "\u{1F493}", "\u{1F497}", "\u{1F496}", "\u{1F498}", "\u{1F49D}", "\u{1F49F}", "\u2665\uFE0F"]
468
+ },
469
+ {
470
+ name: "Food",
471
+ emojis: ["\u{1F354}", "\u{1F35F}", "\u{1F355}", "\u{1F32D}", "\u{1F37F}", "\u{1F9C2}", "\u{1F953}", "\u{1F95A}", "\u{1F373}", "\u{1F9C7}", "\u{1F95E}", "\u{1F9C8}", "\u{1F35E}", "\u{1F950}", "\u{1F968}", "\u{1F96F}", "\u{1F956}", "\u{1F9C0}", "\u{1F957}", "\u{1F959}"]
472
+ },
473
+ {
474
+ name: "Drinks",
475
+ emojis: ["\u2615", "\u{1F375}", "\u{1F376}", "\u{1F37E}", "\u{1F377}", "\u{1F378}", "\u{1F379}", "\u{1F37A}", "\u{1F37B}", "\u{1F942}", "\u{1F943}", "\u{1F964}", "\u{1F9CB}", "\u{1F9C3}", "\u{1F9C9}", "\u{1F9CA}", "\u{1F962}", "\u{1F37D}\uFE0F", "\u{1F374}", "\u{1F944}"]
476
+ },
477
+ {
478
+ name: "Objects",
479
+ emojis: ["\u{1F381}", "\u{1F382}", "\u{1F388}", "\u{1F389}", "\u{1F38A}", "\u{1F380}", "\u{1F4F1}", "\u{1F4BB}", "\u231A", "\u{1F4F7}", "\u{1F511}", "\u{1F4B0}", "\u{1F4B3}", "\u{1F4DD}", "\u{1F4CC}", "\u{1F4CD}", "\u2702\uFE0F", "\u{1F50D}", "\u{1F4A1}", "\u{1F514}"]
480
+ }
481
+ ];
482
+ var EmojiPicker = ({ onSelect, className = "" }) => {
483
+ const [activeCategory, setActiveCategory] = useState4(0);
484
+ const handleSelect = useCallback(
485
+ (emoji) => {
486
+ onSelect(emoji);
487
+ },
488
+ [onSelect]
489
+ );
490
+ return /* @__PURE__ */ jsxs6("div", { className: `bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden ${className}`, children: [
491
+ /* @__PURE__ */ jsx9("div", { className: "flex border-b border-gray-200 overflow-x-auto", children: EMOJI_CATEGORIES.map((category, index) => /* @__PURE__ */ jsxs6(
492
+ "button",
493
+ {
494
+ onClick: () => setActiveCategory(index),
495
+ className: `px-3 py-2 text-xs font-medium whitespace-nowrap border-b-2 transition-colors ${activeCategory === index ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700"}`,
496
+ children: [
497
+ category.emojis[0],
498
+ " ",
499
+ category.name
500
+ ]
501
+ },
502
+ category.name
503
+ )) }),
504
+ /* @__PURE__ */ jsx9("div", { className: "grid grid-cols-10 gap-0.5 p-2 max-h-[240px] overflow-y-auto", children: EMOJI_CATEGORIES[activeCategory].emojis.map((emoji) => /* @__PURE__ */ jsx9(
505
+ "button",
506
+ {
507
+ onClick: () => handleSelect(emoji),
508
+ className: "w-8 h-8 flex items-center justify-center text-lg hover:bg-gray-100 rounded transition-colors cursor-pointer",
509
+ "aria-label": emoji,
510
+ children: emoji
511
+ },
512
+ emoji
513
+ )) })
514
+ ] });
515
+ };
516
+
517
+ // src/MessageComposer.tsx
518
+ import { useState as useState5, useRef as useRef2, useCallback as useCallback2 } from "react";
519
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
520
+ var DEFAULT_ACCEPTED_FILE_TYPES = "image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.zip";
521
+ var MessageComposer = ({
522
+ onSend,
523
+ onAttach,
524
+ value: externalValue,
525
+ onChange: externalOnChange,
526
+ features,
527
+ placeholder = "Digite uma mensagem...",
528
+ maxLength,
529
+ disabled = false,
530
+ acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
531
+ className,
532
+ classNames
533
+ }) => {
534
+ const [internalText, setInternalText] = useState5("");
535
+ const [showEmoji, setShowEmoji] = useState5(false);
536
+ const [attachments, setAttachments] = useState5([]);
537
+ const textareaRef = useRef2(null);
538
+ const fileInputRef = useRef2(null);
539
+ const isControlled = externalValue !== void 0;
540
+ const text = isControlled ? externalValue : internalText;
541
+ const setText = useCallback2((newText) => {
542
+ if (isControlled) {
543
+ externalOnChange?.(newText);
544
+ } else {
545
+ setInternalText(newText);
546
+ }
547
+ }, [isControlled, externalOnChange]);
548
+ const showEmojiButton = features?.emoji !== false;
549
+ const showAttachButton = features?.documents !== false;
550
+ const sendMessage = useCallback2(() => {
551
+ const trimmed = text.trim();
552
+ if (!trimmed && attachments.length === 0) return;
553
+ if (trimmed) onSend(trimmed);
554
+ for (const a of attachments) {
555
+ onAttach?.(a.file);
556
+ URL.revokeObjectURL(a.previewUrl);
557
+ }
558
+ if (!isControlled) setInternalText("");
559
+ setAttachments([]);
560
+ setShowEmoji(false);
561
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
562
+ }, [text, attachments, onSend, onAttach, isControlled]);
563
+ const handleKeyDown = useCallback2((e) => {
564
+ if (e.key === "Enter" && !e.shiftKey) {
565
+ e.preventDefault();
566
+ if (!disabled) sendMessage();
567
+ }
568
+ }, [sendMessage, disabled]);
569
+ const handleInput = useCallback2(() => {
570
+ const ta = textareaRef.current;
571
+ if (!ta) return;
572
+ ta.style.height = "auto";
573
+ ta.style.height = `${Math.min(ta.scrollHeight, 100)}px`;
574
+ }, []);
575
+ const handleEmojiSelect = useCallback2((emoji) => {
576
+ const ta = textareaRef.current;
577
+ if (!ta) {
578
+ setText(text + emoji);
579
+ return;
580
+ }
581
+ const start = ta.selectionStart;
582
+ const end = ta.selectionEnd;
583
+ const newText = text.slice(0, start) + emoji + text.slice(end);
584
+ setText(newText);
585
+ requestAnimationFrame(() => {
586
+ ta.focus();
587
+ ta.setSelectionRange(start + emoji.length, start + emoji.length);
588
+ handleInput();
589
+ });
590
+ }, [text, setText, handleInput]);
591
+ const handleFileChange = useCallback2((e) => {
592
+ const files = e.target.files;
593
+ if (!files) return;
594
+ const previews = [];
595
+ for (let i = 0; i < files.length; i++) {
596
+ const file = files[i];
597
+ previews.push({ file, previewUrl: file.type.startsWith("image/") ? URL.createObjectURL(file) : "" });
598
+ }
599
+ setAttachments((prev) => [...prev, ...previews]);
600
+ if (fileInputRef.current) fileInputRef.current.value = "";
601
+ }, []);
602
+ const removeAttachment = useCallback2((index) => {
603
+ setAttachments((prev) => {
604
+ const next = [...prev];
605
+ if (next[index].previewUrl) URL.revokeObjectURL(next[index].previewUrl);
606
+ next.splice(index, 1);
607
+ return next;
608
+ });
609
+ }, []);
610
+ const insertFormatting = useCallback2((marker) => {
611
+ const ta = textareaRef.current;
612
+ if (!ta) return;
613
+ const start = ta.selectionStart;
614
+ const end = ta.selectionEnd;
615
+ const sel = text.slice(start, end);
616
+ if (sel) {
617
+ setText(text.slice(0, start) + marker + sel + marker + text.slice(end));
618
+ requestAnimationFrame(() => {
619
+ ta.focus();
620
+ ta.setSelectionRange(start + marker.length + sel.length + marker.length, start + marker.length + sel.length + marker.length);
621
+ });
622
+ }
623
+ }, [text, setText]);
624
+ const canSend = text.trim().length > 0 || attachments.length > 0;
625
+ const remaining = maxLength ? maxLength - text.length : null;
626
+ return (
627
+ /* A barra é a superfície (cinza, largura cheia, sem raio) e o campo dentro é que arredonda —
628
+ ordem do WhatsApp. Invertido, o pill arredondado ia até a borda da tela e os cantos
629
+ descobriam o fundo branco da página, que lia como defeito. */
630
+ /* @__PURE__ */ jsxs7("div", { className: cn("bg-[#f0f2f5] px-2 py-2", className), children: [
631
+ attachments.length > 0 && /* @__PURE__ */ jsx10("div", { className: "flex gap-2 px-1 pb-2 overflow-x-auto", children: attachments.map((a, i) => /* @__PURE__ */ jsxs7("div", { className: "relative flex-shrink-0", children: [
632
+ a.previewUrl ? /* @__PURE__ */ jsx10("img", { src: a.previewUrl, alt: "", className: "w-16 h-16 object-cover rounded-lg border border-gray-200" }) : /* @__PURE__ */ jsx10("div", { className: "w-16 h-16 bg-gray-100 rounded-lg border border-gray-200 flex items-center justify-center", children: /* @__PURE__ */ jsxs7("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "1.5", children: [
633
+ /* @__PURE__ */ jsx10("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
634
+ /* @__PURE__ */ jsx10("polyline", { points: "14 2 14 8 20 8" })
635
+ ] }) }),
636
+ /* @__PURE__ */ jsx10("button", { onClick: () => removeAttachment(i), className: "absolute -top-2 -right-2 w-5 h-5 bg-gray-600 text-white rounded-full flex items-center justify-center hover:bg-gray-800 text-xs", children: "\u2715" })
637
+ ] }, i)) }),
638
+ /* @__PURE__ */ jsxs7("div", { className: cn("flex items-end gap-1.5 rounded-xl bg-white px-3 py-2", classNames?.field), children: [
639
+ showEmojiButton && /* @__PURE__ */ jsxs7("div", { className: "relative flex-shrink-0", children: [
640
+ /* @__PURE__ */ jsx10("button", { onClick: () => setShowEmoji((v) => !v), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 transition-colors", "aria-label": "Emoji", children: /* @__PURE__ */ jsxs7("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
641
+ /* @__PURE__ */ jsx10("circle", { cx: "12", cy: "12", r: "10" }),
642
+ /* @__PURE__ */ jsx10("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
643
+ /* @__PURE__ */ jsx10("circle", { cx: "9", cy: "9", r: "0.5", fill: "currentColor" }),
644
+ /* @__PURE__ */ jsx10("circle", { cx: "15", cy: "9", r: "0.5", fill: "currentColor" })
645
+ ] }) }),
646
+ showEmoji && /* @__PURE__ */ jsx10("div", { className: "absolute bottom-full left-0 mb-2 z-10", children: /* @__PURE__ */ jsx10(EmojiPicker, { onSelect: handleEmojiSelect }) })
647
+ ] }),
648
+ /* @__PURE__ */ jsx10(
649
+ "textarea",
650
+ {
651
+ ref: textareaRef,
652
+ value: text,
653
+ onChange: (e) => {
654
+ setText(e.target.value);
655
+ handleInput();
656
+ },
657
+ onKeyDown: handleKeyDown,
658
+ placeholder,
659
+ rows: 1,
660
+ disabled,
661
+ className: "flex-1 resize-none bg-transparent text-[15px] text-[#3b4a54] placeholder-[#8696a0] outline-none py-1.5 max-h-[100px] leading-relaxed",
662
+ style: { fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }
663
+ }
664
+ ),
665
+ showAttachButton && /* @__PURE__ */ jsxs7(Fragment2, { children: [
666
+ /* @__PURE__ */ jsx10("input", { ref: fileInputRef, type: "file", multiple: true, accept: acceptedFileTypes, onChange: handleFileChange, className: "hidden" }),
667
+ /* @__PURE__ */ jsx10("button", { onClick: () => fileInputRef.current?.click(), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 flex-shrink-0 transition-colors", "aria-label": "Anexar", children: /* @__PURE__ */ jsx10("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx10("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) }) })
668
+ ] }),
669
+ /* @__PURE__ */ jsx10(
670
+ "button",
671
+ {
672
+ onClick: sendMessage,
673
+ disabled: !canSend || disabled,
674
+ className: `w-10 h-10 flex items-center justify-center rounded-full flex-shrink-0 transition-all ${canSend && !disabled ? "bg-[#00a884] text-white hover:bg-[#06cf9c] shadow-sm" : "bg-gray-200 text-gray-400 cursor-not-allowed"}`,
675
+ "aria-label": "Enviar",
676
+ children: /* @__PURE__ */ jsx10("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx10("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
677
+ }
678
+ )
679
+ ] }),
680
+ remaining !== null && /* @__PURE__ */ jsx10("div", { className: "flex justify-end mt-1 pr-1", children: /* @__PURE__ */ jsx10("span", { className: `text-xs ${remaining < 20 ? "text-red-500" : "text-gray-400"}`, children: remaining }) })
681
+ ] })
682
+ );
683
+ };
684
+
685
+ // src/DateDivider.tsx
686
+ import { jsx as jsx11 } from "react/jsx-runtime";
687
+ function DateDivider({ iso, className, classNames }) {
688
+ const { dateDivider } = useConversationLocales();
689
+ return /* @__PURE__ */ jsx11("div", { className: cn("flex justify-center sticky top-0 z-10 my-2 pointer-events-none", classNames?.root, className), children: /* @__PURE__ */ jsx11(
690
+ "span",
691
+ {
692
+ className: cn(
693
+ "bg-white/95 dark:bg-gray-800/95 text-gray-600 dark:text-gray-300 text-xs font-medium px-3 py-1 rounded-lg shadow-sm",
694
+ classNames?.label
695
+ ),
696
+ children: formatDividerLabel(iso, dateDivider)
697
+ }
698
+ ) });
699
+ }
700
+ function formatDividerLabel(iso, dateDivider) {
701
+ const date = new Date(iso);
702
+ const today = /* @__PURE__ */ new Date();
703
+ const yesterday = new Date(today);
704
+ yesterday.setDate(today.getDate() - 1);
705
+ if (isSameDay(date, today)) return dateDivider.today;
706
+ if (isSameDay(date, yesterday)) return dateDivider.yesterday;
707
+ return date.toLocaleDateString("pt-BR", { day: "2-digit", month: "long", year: "numeric" });
708
+ }
709
+
710
+ export {
711
+ ConversationLocalesProvider,
712
+ useConversationLocales,
713
+ StatusTicks,
714
+ AudioPlayer,
715
+ FileIcon,
716
+ formatTimestamp,
717
+ formatFileSize,
718
+ MediaRenderer,
719
+ Lightbox,
720
+ cn,
721
+ MessageBubble,
722
+ ConversationWallpaper,
723
+ EmojiPicker,
724
+ MessageComposer,
725
+ DateDivider
726
+ };