@adatechnology/conversations-ui 0.1.0-rc.1 → 0.1.0-rc.3

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