@better-zap/react 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,14 +28,30 @@ let tailwind_merge = require("tailwind-merge");
28
28
  let react_jsx_runtime = require("react/jsx-runtime");
29
29
  let _hugeicons_react = require("@hugeicons/react");
30
30
  let _hugeicons_core_free_icons = require("@hugeicons/core-free-icons");
31
+ let _legendapp_list_react = require("@legendapp/list/react");
31
32
  let class_variance_authority = require("class-variance-authority");
32
33
  let better_zap = require("better-zap");
33
- //#region src/react/utils.ts
34
+ //#region src/utils.ts
34
35
  function cn(...inputs) {
35
36
  return (0, tailwind_merge.twMerge)((0, clsx.clsx)(inputs));
36
37
  }
38
+ function getDisplayDate(dateStr) {
39
+ const dateObj = new Date(dateStr);
40
+ const now = /* @__PURE__ */ new Date();
41
+ const isToday = dateObj.toDateString() === now.toDateString();
42
+ const yesterday = new Date(now);
43
+ yesterday.setDate(yesterday.getDate() - 1);
44
+ const isYesterday = dateObj.toDateString() === yesterday.toDateString();
45
+ if (isToday) return "HOJE";
46
+ else if (isYesterday) return "ONTEM";
47
+ else return dateObj.toLocaleDateString("pt-BR", {
48
+ day: "2-digit",
49
+ month: "2-digit",
50
+ year: "numeric"
51
+ });
52
+ }
37
53
  //#endregion
38
- //#region src/react/whatsapp-dashboard.tsx
54
+ //#region src/whatsapp-dashboard.tsx
39
55
  const MOBILE_BREAKPOINT = 1024;
40
56
  function useIsMobile() {
41
57
  const [isMobile, setIsMobile] = (0, react.useState)(false);
@@ -76,7 +92,7 @@ function WhatsappDashboard({ children, className, defaultMobileView = "list", ..
76
92
  });
77
93
  }
78
94
  //#endregion
79
- //#region src/react/message-bubble.tsx
95
+ //#region src/message-bubble.tsx
80
96
  const bubbleVariants = (0, class_variance_authority.cva)("relative shadow-[0_1px_0.5px_rgba(11,20,26,0.13)] rounded-lg px-3 py-2 max-w-[65%]", { variants: { variant: {
81
97
  outgoing: "bg-green-100 text-green-900 rounded-tr-none",
82
98
  incoming: "bg-gray-100 text-gray-900 rounded-tl-none",
@@ -174,24 +190,7 @@ function FormattedMessage({ text }) {
174
190
  }) });
175
191
  }
176
192
  //#endregion
177
- //#region src/date.ts
178
- function getDisplayDate(dateStr) {
179
- const dateObj = new Date(dateStr);
180
- const now = /* @__PURE__ */ new Date();
181
- const isToday = dateObj.toDateString() === now.toDateString();
182
- const yesterday = new Date(now);
183
- yesterday.setDate(yesterday.getDate() - 1);
184
- const isYesterday = dateObj.toDateString() === yesterday.toDateString();
185
- if (isToday) return "HOJE";
186
- else if (isYesterday) return "ONTEM";
187
- else return dateObj.toLocaleDateString("pt-BR", {
188
- day: "2-digit",
189
- month: "2-digit",
190
- year: "numeric"
191
- });
192
- }
193
- //#endregion
194
- //#region src/react/message-view.tsx
193
+ //#region src/message-view.tsx
195
194
  function MessageView({ children, className, ...props }) {
196
195
  const { isMobile, mobileView } = useWhatsappDashboard();
197
196
  const hasContent = react.default.Children.count(children) > 0;
@@ -258,70 +257,80 @@ function MessageViewHeader({ conversation, onBack, onInfoClick, className }) {
258
257
  })]
259
258
  });
260
259
  }
261
- const SCROLL_TOP_THRESHOLD = 50;
260
+ const MessageViewScrollContext = react.default.createContext(null);
262
261
  function MessageViewContent({ children, autoScroll = true, onScrollTop, className, ...props }) {
263
- const scrollRef = (0, react.useRef)(null);
264
- const prevScrollHeightRef = (0, react.useRef)(0);
265
- (0, react.useEffect)(() => {
266
- if (autoScroll && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
267
- }, [children, autoScroll]);
268
- (0, react.useEffect)(() => {
269
- const el = scrollRef.current;
270
- if (!el) return;
271
- const newScrollHeight = el.scrollHeight;
272
- const prevScrollHeight = prevScrollHeightRef.current;
273
- if (prevScrollHeight > 0 && newScrollHeight > prevScrollHeight) {
274
- const addedHeight = newScrollHeight - prevScrollHeight;
275
- if (el.scrollTop < SCROLL_TOP_THRESHOLD + addedHeight) el.scrollTop = addedHeight;
276
- }
277
- prevScrollHeightRef.current = newScrollHeight;
278
- });
279
- const handleScroll = (0, react.useCallback)(() => {
280
- const el = scrollRef.current;
281
- if (!el || !onScrollTop) return;
282
- if (el.scrollTop <= SCROLL_TOP_THRESHOLD) onScrollTop();
283
- }, [onScrollTop]);
284
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
285
- ref: scrollRef,
286
- className: cn("flex flex-1 flex-col overflow-y-auto p-4 pb-0 chat-scrollbar", className),
287
- onScroll: handleScroll,
288
- ...props,
289
- children
262
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageViewScrollContext.Provider, {
263
+ value: {
264
+ autoScroll,
265
+ onScrollTop
266
+ },
267
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
268
+ className: cn("flex min-h-0 flex-1 flex-col p-4 pb-0", className),
269
+ ...props,
270
+ children
271
+ })
290
272
  });
291
273
  }
292
274
  function MessageList({ messages, renderMessageLabel, className }) {
293
- const messageGroups = (0, react.useMemo)(() => {
294
- const groups = [];
295
- let currentGroup = null;
275
+ const scrollContext = (0, react.useContext)(MessageViewScrollContext);
276
+ const autoScroll = scrollContext?.autoScroll ?? true;
277
+ const onScrollTop = scrollContext?.onScrollTop;
278
+ const { items, stickyHeaderIndices } = (0, react.useMemo)(() => {
279
+ const itemsNew = [];
280
+ const stickyHeaderIndicesNew = [];
281
+ let currentDate = null;
296
282
  messages.forEach((msg) => {
297
283
  const displayDate = getDisplayDate(msg.sentAt);
298
- if (!currentGroup || currentGroup.date !== displayDate) {
299
- currentGroup = {
300
- date: displayDate,
301
- messages: []
302
- };
303
- groups.push(currentGroup);
284
+ if (currentDate !== displayDate) {
285
+ currentDate = displayDate;
286
+ stickyHeaderIndicesNew.push(itemsNew.length);
287
+ itemsNew.push({
288
+ type: "date",
289
+ id: `date:${displayDate}`,
290
+ date: displayDate
291
+ });
304
292
  }
305
- currentGroup.messages.push(msg);
293
+ itemsNew.push({
294
+ type: "message",
295
+ id: msg.id,
296
+ message: msg
297
+ });
306
298
  });
307
- return groups;
299
+ return {
300
+ items: itemsNew,
301
+ stickyHeaderIndices: stickyHeaderIndicesNew
302
+ };
308
303
  }, [messages]);
309
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
310
- className: cn("flex flex-col pb-4", className),
311
- children: messageGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
312
- className: "flex flex-col gap-1",
313
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DateDivider, { date: group.date }), group.messages.map((msg) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, {
314
- content: msg.content || "",
315
- sender: msg.direction === "incoming" ? "user" : "bot",
316
- timestamp: new Date(msg.sentAt).toLocaleTimeString("pt-BR", {
317
- hour: "2-digit",
318
- minute: "2-digit"
319
- }),
320
- status: msg.status,
321
- templateName: msg.templateName || void 0,
322
- label: renderMessageLabel?.(msg)
323
- }, msg.id))]
324
- }, group.date))
304
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_legendapp_list_react.LegendList, {
305
+ alignItemsAtEnd: true,
306
+ className: cn("chat-scrollbar", className),
307
+ contentContainerStyle: { paddingBottom: 16 },
308
+ data: items,
309
+ estimatedItemSize: 72,
310
+ getItemType: (item) => item.type,
311
+ initialScrollAtEnd: autoScroll,
312
+ keyExtractor: (item) => item.id,
313
+ maintainScrollAtEnd: autoScroll,
314
+ maintainVisibleContentPosition: true,
315
+ onStartReached: onScrollTop ? () => onScrollTop() : void 0,
316
+ onStartReachedThreshold: .1,
317
+ recycleItems: true,
318
+ renderItem: ({ item }) => item.type === "date" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DateDivider, { date: item.date }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, {
319
+ content: item.message.content || "",
320
+ sender: item.message.direction === "incoming" ? "user" : "bot",
321
+ timestamp: new Date(item.message.sentAt).toLocaleTimeString("pt-BR", {
322
+ hour: "2-digit",
323
+ minute: "2-digit"
324
+ }),
325
+ status: item.message.status,
326
+ templateName: item.message.templateName || void 0,
327
+ label: renderMessageLabel?.(item.message)
328
+ }),
329
+ stickyHeaderIndices,
330
+ style: {
331
+ height: "100%",
332
+ minHeight: 0
333
+ }
325
334
  });
326
335
  }
327
336
  function DateDivider({ date }) {
@@ -368,7 +377,7 @@ function MessageViewEmpty({ className, ...props }) {
368
377
  });
369
378
  }
370
379
  //#endregion
371
- //#region src/react/conversation-search.tsx
380
+ //#region src/conversation-search.tsx
372
381
  function ConversationSearch({ value, onChange, className }) {
373
382
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
374
383
  className: cn("border-b border-[#e9edef] shrink-0 p-2", className),
@@ -389,11 +398,46 @@ function ConversationSearch({ value, onChange, className }) {
389
398
  });
390
399
  }
391
400
  //#endregion
392
- //#region src/react/conversation-list.tsx
401
+ //#region src/conversation-filter-chips.tsx
402
+ const chips = [{
403
+ label: "Tudo",
404
+ value: "all"
405
+ }, {
406
+ label: "Não lidas",
407
+ value: "unread"
408
+ }];
409
+ function ConversationFilterChips({ value, onValueChange, unreadCount = 0, className }) {
410
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
411
+ className: cn("flex items-center gap-2 px-3 py-3", className),
412
+ children: chips.map((chip) => {
413
+ const isActive = chip.value === value;
414
+ const showCount = chip.value === "unread" && unreadCount > 0;
415
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
416
+ type: "button",
417
+ onClick: () => onValueChange(chip.value),
418
+ "aria-pressed": isActive,
419
+ className: cn("inline-flex h-8 items-center rounded-full border px-4 text-[15px] font-medium transition-colors", isActive ? "border-[#b8e6c1] bg-[#e7fce3] text-[#017561]" : "border-[#d1d7db] bg-white text-[#54656f] hover:bg-[#f5f6f6]"),
420
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: chip.label }), showCount ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
421
+ className: "ml-1",
422
+ children: unreadCount
423
+ }) : null]
424
+ }, chip.value);
425
+ })
426
+ });
427
+ }
428
+ //#endregion
429
+ //#region src/conversation-list.tsx
393
430
  function ConversationList({ conversations, isLoading, isError, selectedConversationId, onSelect, className }) {
394
431
  const { isMobile, mobileView, setMobileView } = useWhatsappDashboard();
395
432
  const [search, setSearch] = (0, react.useState)("");
396
- const filtered = conversations.filter((c) => c.phone.includes(search) || c.contactName?.toLowerCase().includes(search.toLowerCase()));
433
+ const [filter, setFilter] = (0, react.useState)("all");
434
+ const normalizedSearch = search.trim().toLowerCase();
435
+ const unreadConversationsCount = conversations.filter((c) => c.unreadCount > 0).length;
436
+ const filtered = conversations.filter((conversation) => {
437
+ const matchesSearch = normalizedSearch.length === 0 || conversation.phone.toLowerCase().includes(normalizedSearch) || conversation.contactName?.toLowerCase().includes(normalizedSearch);
438
+ const matchesFilter = filter === "all" || conversation.unreadCount > 0;
439
+ return matchesSearch && matchesFilter;
440
+ });
397
441
  const handleSelect = (id) => {
398
442
  onSelect(id);
399
443
  setMobileView("chat");
@@ -402,32 +446,53 @@ function ConversationList({ conversations, isLoading, isError, selectedConversat
402
446
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
403
447
  className: cn("flex flex-col h-full bg-white border-r border-[#e9edef]", isMobile ? "w-full" : "min-w-[320px] max-w-105", className),
404
448
  style: isVisible ? void 0 : { display: "none" },
405
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationSearch, {
406
- value: search,
407
- onChange: setSearch
408
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
409
- className: "flex-1 overflow-y-auto overflow-x-hidden chat-scrollbar",
410
- children: isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
411
- className: "flex items-center justify-center h-full text-sm text-[#667781]",
412
- children: "Carregando..."
413
- }) : isError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
414
- className: "flex items-center justify-center h-full text-sm text-red-500",
415
- children: "Erro ao carregar conversas"
416
- }) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
417
- className: "flex flex-col items-center justify-center h-full gap-2 text-[#667781]",
418
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
419
- icon: _hugeicons_core_free_icons.Message01Icon,
420
- size: 32
421
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
422
- className: "text-sm",
423
- children: "Nenhuma conversa encontrada"
424
- })]
425
- }) : filtered.map((conversation) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationItem, {
426
- conversation,
427
- isSelected: selectedConversationId === conversation.id,
428
- onClick: () => handleSelect(conversation.id)
429
- }, conversation.id))
430
- })]
449
+ children: [
450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationSearch, {
451
+ value: search,
452
+ onChange: setSearch
453
+ }),
454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationFilterChips, {
455
+ value: filter,
456
+ onValueChange: setFilter,
457
+ unreadCount: unreadConversationsCount
458
+ }),
459
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
460
+ className: "min-h-0 flex-1",
461
+ children: isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
462
+ className: "flex items-center justify-center h-full text-sm text-[#667781]",
463
+ children: "Carregando..."
464
+ }) : isError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
465
+ className: "flex items-center justify-center h-full text-sm text-red-500",
466
+ children: "Erro ao carregar conversas"
467
+ }) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
468
+ className: "flex flex-col items-center justify-center h-full gap-2 text-[#667781]",
469
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
470
+ icon: _hugeicons_core_free_icons.Message01Icon,
471
+ size: 32
472
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
473
+ className: "text-sm",
474
+ children: "Nenhuma conversa encontrada"
475
+ })]
476
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_legendapp_list_react.LegendList, {
477
+ className: "chat-scrollbar",
478
+ data: filtered,
479
+ estimatedItemSize: 72,
480
+ extraData: selectedConversationId,
481
+ getFixedItemSize: () => 72,
482
+ keyExtractor: (conversation) => conversation.id,
483
+ recycleItems: true,
484
+ renderItem: ({ item: conversation }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationItem, {
485
+ conversation,
486
+ isSelected: selectedConversationId === conversation.id,
487
+ onClick: () => handleSelect(conversation.id)
488
+ }),
489
+ style: {
490
+ height: "100%",
491
+ overflowX: "hidden"
492
+ }
493
+ })
494
+ })
495
+ ]
431
496
  });
432
497
  }
433
498
  function ConversationItem({ conversation, isSelected, onClick }) {
@@ -490,7 +555,7 @@ function formatTime(dateStr) {
490
555
  }
491
556
  }
492
557
  //#endregion
493
- //#region src/react/message-input.tsx
558
+ //#region src/message-input.tsx
494
559
  function MessageInput({ onSend, conversation, messages, disabled, placeholder = "Digite uma mensagem", className, contextWindowOpen = true }) {
495
560
  const [text, setText] = (0, react.useState)("");
496
561
  const [isSending, setIsSending] = (0, react.useState)(false);
@@ -605,6 +670,7 @@ function MessageInput({ onSend, conversation, messages, disabled, placeholder =
605
670
  });
606
671
  }
607
672
  //#endregion
673
+ exports.ConversationFilterChips = ConversationFilterChips;
608
674
  exports.ConversationItem = ConversationItem;
609
675
  exports.ConversationList = ConversationList;
610
676
  exports.FormattedMessage = FormattedMessage;
@@ -617,4 +683,5 @@ exports.MessageViewEmpty = MessageViewEmpty;
617
683
  exports.MessageViewHeader = MessageViewHeader;
618
684
  exports.WhatsappDashboard = WhatsappDashboard;
619
685
  exports.cn = cn;
686
+ exports.getDisplayDate = getDisplayDate;
620
687
  exports.useWhatsappDashboard = useWhatsappDashboard;
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { Conversation, Conversation as Conversation$1, ConversationRecord, FreeformMessageWindow, UIMessage, UIMessage as UIMessage$1, UIMessageStatus, UIMessageStatus as UIMessageStatus$1 } from "better-zap";
4
4
  import { ClassValue } from "clsx";
5
5
 
6
- //#region src/react/whatsapp-dashboard.d.ts
6
+ //#region src/whatsapp-dashboard.d.ts
7
7
  type MobileView = "list" | "chat";
8
8
  interface WhatsappDashboardContextValue {
9
9
  isMobile: boolean;
@@ -22,7 +22,7 @@ declare function WhatsappDashboard({
22
22
  ...props
23
23
  }: WhatsappDashboardProps): react_jsx_runtime0.JSX.Element;
24
24
  //#endregion
25
- //#region src/react/message-view.d.ts
25
+ //#region src/message-view.d.ts
26
26
  interface MessageViewProps extends React.HTMLAttributes<HTMLDivElement> {
27
27
  children?: React.ReactNode;
28
28
  }
@@ -71,7 +71,7 @@ declare function MessageViewEmpty({
71
71
  ...props
72
72
  }: React.HTMLAttributes<HTMLDivElement>): react_jsx_runtime0.JSX.Element;
73
73
  //#endregion
74
- //#region src/react/message-bubble.d.ts
74
+ //#region src/message-bubble.d.ts
75
75
  interface MessageBubbleProps extends React.HTMLAttributes<HTMLDivElement> {
76
76
  content: string;
77
77
  sender: "user" | "bot";
@@ -96,7 +96,7 @@ declare function FormattedMessage({
96
96
  text: string;
97
97
  }): react_jsx_runtime0.JSX.Element | null;
98
98
  //#endregion
99
- //#region src/react/conversation-list.d.ts
99
+ //#region src/conversation-list.d.ts
100
100
  interface ConversationListProps {
101
101
  conversations: Conversation$1[];
102
102
  isLoading: boolean;
@@ -124,10 +124,26 @@ declare function ConversationItem({
124
124
  onClick
125
125
  }: ConversationItemProps): react_jsx_runtime0.JSX.Element;
126
126
  //#endregion
127
- //#region src/react/utils.d.ts
127
+ //#region src/conversation-filter-chips.d.ts
128
+ type ConversationFilterValue = "all" | "unread";
129
+ interface ConversationFilterChipsProps {
130
+ value: ConversationFilterValue;
131
+ onValueChange: (value: ConversationFilterValue) => void;
132
+ unreadCount?: number;
133
+ className?: string;
134
+ }
135
+ declare function ConversationFilterChips({
136
+ value,
137
+ onValueChange,
138
+ unreadCount,
139
+ className
140
+ }: ConversationFilterChipsProps): react_jsx_runtime0.JSX.Element;
141
+ //#endregion
142
+ //#region src/utils.d.ts
128
143
  declare function cn(...inputs: ClassValue[]): string;
144
+ declare function getDisplayDate(dateStr: string): string;
129
145
  //#endregion
130
- //#region src/react/message-input.d.ts
146
+ //#region src/message-input.d.ts
131
147
  interface MessageInputProps {
132
148
  onSend: (text: string) => void | Promise<void>;
133
149
  conversation?: Conversation$1 | null;
@@ -148,4 +164,4 @@ declare function MessageInput({
148
164
  contextWindowOpen
149
165
  }: MessageInputProps): react_jsx_runtime0.JSX.Element;
150
166
  //#endregion
151
- export { type Conversation, ConversationItem, ConversationList, type ConversationRecord, FormattedMessage, type FreeformMessageWindow, MessageBubble, MessageBubbleProps, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, type UIMessage, type UIMessageStatus, WhatsappDashboard, cn, useWhatsappDashboard };
167
+ export { type Conversation, ConversationFilterChips, ConversationFilterValue, ConversationItem, ConversationList, type ConversationRecord, FormattedMessage, type FreeformMessageWindow, MessageBubble, MessageBubbleProps, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, type UIMessage, type UIMessageStatus, WhatsappDashboard, cn, getDisplayDate, useWhatsappDashboard };
package/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@ import { ClassValue } from "clsx";
3
3
  import * as react_jsx_runtime0 from "react/jsx-runtime";
4
4
  import { Conversation, Conversation as Conversation$1, ConversationRecord, FreeformMessageWindow, UIMessage, UIMessage as UIMessage$1, UIMessageStatus, UIMessageStatus as UIMessageStatus$1 } from "better-zap";
5
5
 
6
- //#region src/react/whatsapp-dashboard.d.ts
6
+ //#region src/whatsapp-dashboard.d.ts
7
7
  type MobileView = "list" | "chat";
8
8
  interface WhatsappDashboardContextValue {
9
9
  isMobile: boolean;
@@ -22,7 +22,7 @@ declare function WhatsappDashboard({
22
22
  ...props
23
23
  }: WhatsappDashboardProps): react_jsx_runtime0.JSX.Element;
24
24
  //#endregion
25
- //#region src/react/message-view.d.ts
25
+ //#region src/message-view.d.ts
26
26
  interface MessageViewProps extends React.HTMLAttributes<HTMLDivElement> {
27
27
  children?: React.ReactNode;
28
28
  }
@@ -71,7 +71,7 @@ declare function MessageViewEmpty({
71
71
  ...props
72
72
  }: React.HTMLAttributes<HTMLDivElement>): react_jsx_runtime0.JSX.Element;
73
73
  //#endregion
74
- //#region src/react/message-bubble.d.ts
74
+ //#region src/message-bubble.d.ts
75
75
  interface MessageBubbleProps extends React.HTMLAttributes<HTMLDivElement> {
76
76
  content: string;
77
77
  sender: "user" | "bot";
@@ -96,7 +96,7 @@ declare function FormattedMessage({
96
96
  text: string;
97
97
  }): react_jsx_runtime0.JSX.Element | null;
98
98
  //#endregion
99
- //#region src/react/conversation-list.d.ts
99
+ //#region src/conversation-list.d.ts
100
100
  interface ConversationListProps {
101
101
  conversations: Conversation$1[];
102
102
  isLoading: boolean;
@@ -124,10 +124,26 @@ declare function ConversationItem({
124
124
  onClick
125
125
  }: ConversationItemProps): react_jsx_runtime0.JSX.Element;
126
126
  //#endregion
127
- //#region src/react/utils.d.ts
127
+ //#region src/conversation-filter-chips.d.ts
128
+ type ConversationFilterValue = "all" | "unread";
129
+ interface ConversationFilterChipsProps {
130
+ value: ConversationFilterValue;
131
+ onValueChange: (value: ConversationFilterValue) => void;
132
+ unreadCount?: number;
133
+ className?: string;
134
+ }
135
+ declare function ConversationFilterChips({
136
+ value,
137
+ onValueChange,
138
+ unreadCount,
139
+ className
140
+ }: ConversationFilterChipsProps): react_jsx_runtime0.JSX.Element;
141
+ //#endregion
142
+ //#region src/utils.d.ts
128
143
  declare function cn(...inputs: ClassValue[]): string;
144
+ declare function getDisplayDate(dateStr: string): string;
129
145
  //#endregion
130
- //#region src/react/message-input.d.ts
146
+ //#region src/message-input.d.ts
131
147
  interface MessageInputProps {
132
148
  onSend: (text: string) => void | Promise<void>;
133
149
  conversation?: Conversation$1 | null;
@@ -148,4 +164,4 @@ declare function MessageInput({
148
164
  contextWindowOpen
149
165
  }: MessageInputProps): react_jsx_runtime0.JSX.Element;
150
166
  //#endregion
151
- export { type Conversation, ConversationItem, ConversationList, type ConversationRecord, FormattedMessage, type FreeformMessageWindow, MessageBubble, MessageBubbleProps, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, type UIMessage, type UIMessageStatus, WhatsappDashboard, cn, useWhatsappDashboard };
167
+ export { type Conversation, ConversationFilterChips, ConversationFilterValue, ConversationItem, ConversationList, type ConversationRecord, FormattedMessage, type FreeformMessageWindow, MessageBubble, MessageBubbleProps, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, type UIMessage, type UIMessageStatus, WhatsappDashboard, cn, getDisplayDate, useWhatsappDashboard };
package/dist/index.mjs CHANGED
@@ -4,14 +4,30 @@ import { twMerge } from "tailwind-merge";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { HugeiconsIcon } from "@hugeicons/react";
6
6
  import { Add01Icon, ArrowLeft02Icon, Clock01Icon, InformationCircleIcon, Message01Icon, Mic01Icon, Search01Icon, Sent02Icon, SmileIcon, UserIcon } from "@hugeicons/core-free-icons";
7
+ import { LegendList } from "@legendapp/list/react";
7
8
  import { cva } from "class-variance-authority";
8
9
  import { resolveConversationFreeformMessageWindow } from "better-zap";
9
- //#region src/react/utils.ts
10
+ //#region src/utils.ts
10
11
  function cn(...inputs) {
11
12
  return twMerge(clsx(inputs));
12
13
  }
14
+ function getDisplayDate(dateStr) {
15
+ const dateObj = new Date(dateStr);
16
+ const now = /* @__PURE__ */ new Date();
17
+ const isToday = dateObj.toDateString() === now.toDateString();
18
+ const yesterday = new Date(now);
19
+ yesterday.setDate(yesterday.getDate() - 1);
20
+ const isYesterday = dateObj.toDateString() === yesterday.toDateString();
21
+ if (isToday) return "HOJE";
22
+ else if (isYesterday) return "ONTEM";
23
+ else return dateObj.toLocaleDateString("pt-BR", {
24
+ day: "2-digit",
25
+ month: "2-digit",
26
+ year: "numeric"
27
+ });
28
+ }
13
29
  //#endregion
14
- //#region src/react/whatsapp-dashboard.tsx
30
+ //#region src/whatsapp-dashboard.tsx
15
31
  const MOBILE_BREAKPOINT = 1024;
16
32
  function useIsMobile() {
17
33
  const [isMobile, setIsMobile] = useState(false);
@@ -52,7 +68,7 @@ function WhatsappDashboard({ children, className, defaultMobileView = "list", ..
52
68
  });
53
69
  }
54
70
  //#endregion
55
- //#region src/react/message-bubble.tsx
71
+ //#region src/message-bubble.tsx
56
72
  const bubbleVariants = cva("relative shadow-[0_1px_0.5px_rgba(11,20,26,0.13)] rounded-lg px-3 py-2 max-w-[65%]", { variants: { variant: {
57
73
  outgoing: "bg-green-100 text-green-900 rounded-tr-none",
58
74
  incoming: "bg-gray-100 text-gray-900 rounded-tl-none",
@@ -150,24 +166,7 @@ function FormattedMessage({ text }) {
150
166
  }) });
151
167
  }
152
168
  //#endregion
153
- //#region src/date.ts
154
- function getDisplayDate(dateStr) {
155
- const dateObj = new Date(dateStr);
156
- const now = /* @__PURE__ */ new Date();
157
- const isToday = dateObj.toDateString() === now.toDateString();
158
- const yesterday = new Date(now);
159
- yesterday.setDate(yesterday.getDate() - 1);
160
- const isYesterday = dateObj.toDateString() === yesterday.toDateString();
161
- if (isToday) return "HOJE";
162
- else if (isYesterday) return "ONTEM";
163
- else return dateObj.toLocaleDateString("pt-BR", {
164
- day: "2-digit",
165
- month: "2-digit",
166
- year: "numeric"
167
- });
168
- }
169
- //#endregion
170
- //#region src/react/message-view.tsx
169
+ //#region src/message-view.tsx
171
170
  function MessageView({ children, className, ...props }) {
172
171
  const { isMobile, mobileView } = useWhatsappDashboard();
173
172
  const hasContent = React.Children.count(children) > 0;
@@ -234,70 +233,80 @@ function MessageViewHeader({ conversation, onBack, onInfoClick, className }) {
234
233
  })]
235
234
  });
236
235
  }
237
- const SCROLL_TOP_THRESHOLD = 50;
236
+ const MessageViewScrollContext = React.createContext(null);
238
237
  function MessageViewContent({ children, autoScroll = true, onScrollTop, className, ...props }) {
239
- const scrollRef = useRef(null);
240
- const prevScrollHeightRef = useRef(0);
241
- useEffect(() => {
242
- if (autoScroll && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
243
- }, [children, autoScroll]);
244
- useEffect(() => {
245
- const el = scrollRef.current;
246
- if (!el) return;
247
- const newScrollHeight = el.scrollHeight;
248
- const prevScrollHeight = prevScrollHeightRef.current;
249
- if (prevScrollHeight > 0 && newScrollHeight > prevScrollHeight) {
250
- const addedHeight = newScrollHeight - prevScrollHeight;
251
- if (el.scrollTop < SCROLL_TOP_THRESHOLD + addedHeight) el.scrollTop = addedHeight;
252
- }
253
- prevScrollHeightRef.current = newScrollHeight;
254
- });
255
- const handleScroll = useCallback(() => {
256
- const el = scrollRef.current;
257
- if (!el || !onScrollTop) return;
258
- if (el.scrollTop <= SCROLL_TOP_THRESHOLD) onScrollTop();
259
- }, [onScrollTop]);
260
- return /* @__PURE__ */ jsx("div", {
261
- ref: scrollRef,
262
- className: cn("flex flex-1 flex-col overflow-y-auto p-4 pb-0 chat-scrollbar", className),
263
- onScroll: handleScroll,
264
- ...props,
265
- children
238
+ return /* @__PURE__ */ jsx(MessageViewScrollContext.Provider, {
239
+ value: {
240
+ autoScroll,
241
+ onScrollTop
242
+ },
243
+ children: /* @__PURE__ */ jsx("div", {
244
+ className: cn("flex min-h-0 flex-1 flex-col p-4 pb-0", className),
245
+ ...props,
246
+ children
247
+ })
266
248
  });
267
249
  }
268
250
  function MessageList({ messages, renderMessageLabel, className }) {
269
- const messageGroups = useMemo(() => {
270
- const groups = [];
271
- let currentGroup = null;
251
+ const scrollContext = useContext(MessageViewScrollContext);
252
+ const autoScroll = scrollContext?.autoScroll ?? true;
253
+ const onScrollTop = scrollContext?.onScrollTop;
254
+ const { items, stickyHeaderIndices } = useMemo(() => {
255
+ const itemsNew = [];
256
+ const stickyHeaderIndicesNew = [];
257
+ let currentDate = null;
272
258
  messages.forEach((msg) => {
273
259
  const displayDate = getDisplayDate(msg.sentAt);
274
- if (!currentGroup || currentGroup.date !== displayDate) {
275
- currentGroup = {
276
- date: displayDate,
277
- messages: []
278
- };
279
- groups.push(currentGroup);
260
+ if (currentDate !== displayDate) {
261
+ currentDate = displayDate;
262
+ stickyHeaderIndicesNew.push(itemsNew.length);
263
+ itemsNew.push({
264
+ type: "date",
265
+ id: `date:${displayDate}`,
266
+ date: displayDate
267
+ });
280
268
  }
281
- currentGroup.messages.push(msg);
269
+ itemsNew.push({
270
+ type: "message",
271
+ id: msg.id,
272
+ message: msg
273
+ });
282
274
  });
283
- return groups;
275
+ return {
276
+ items: itemsNew,
277
+ stickyHeaderIndices: stickyHeaderIndicesNew
278
+ };
284
279
  }, [messages]);
285
- return /* @__PURE__ */ jsx("div", {
286
- className: cn("flex flex-col pb-4", className),
287
- children: messageGroups.map((group) => /* @__PURE__ */ jsxs("div", {
288
- className: "flex flex-col gap-1",
289
- children: [/* @__PURE__ */ jsx(DateDivider, { date: group.date }), group.messages.map((msg) => /* @__PURE__ */ jsx(MessageBubble, {
290
- content: msg.content || "",
291
- sender: msg.direction === "incoming" ? "user" : "bot",
292
- timestamp: new Date(msg.sentAt).toLocaleTimeString("pt-BR", {
293
- hour: "2-digit",
294
- minute: "2-digit"
295
- }),
296
- status: msg.status,
297
- templateName: msg.templateName || void 0,
298
- label: renderMessageLabel?.(msg)
299
- }, msg.id))]
300
- }, group.date))
280
+ return /* @__PURE__ */ jsx(LegendList, {
281
+ alignItemsAtEnd: true,
282
+ className: cn("chat-scrollbar", className),
283
+ contentContainerStyle: { paddingBottom: 16 },
284
+ data: items,
285
+ estimatedItemSize: 72,
286
+ getItemType: (item) => item.type,
287
+ initialScrollAtEnd: autoScroll,
288
+ keyExtractor: (item) => item.id,
289
+ maintainScrollAtEnd: autoScroll,
290
+ maintainVisibleContentPosition: true,
291
+ onStartReached: onScrollTop ? () => onScrollTop() : void 0,
292
+ onStartReachedThreshold: .1,
293
+ recycleItems: true,
294
+ renderItem: ({ item }) => item.type === "date" ? /* @__PURE__ */ jsx(DateDivider, { date: item.date }) : /* @__PURE__ */ jsx(MessageBubble, {
295
+ content: item.message.content || "",
296
+ sender: item.message.direction === "incoming" ? "user" : "bot",
297
+ timestamp: new Date(item.message.sentAt).toLocaleTimeString("pt-BR", {
298
+ hour: "2-digit",
299
+ minute: "2-digit"
300
+ }),
301
+ status: item.message.status,
302
+ templateName: item.message.templateName || void 0,
303
+ label: renderMessageLabel?.(item.message)
304
+ }),
305
+ stickyHeaderIndices,
306
+ style: {
307
+ height: "100%",
308
+ minHeight: 0
309
+ }
301
310
  });
302
311
  }
303
312
  function DateDivider({ date }) {
@@ -344,7 +353,7 @@ function MessageViewEmpty({ className, ...props }) {
344
353
  });
345
354
  }
346
355
  //#endregion
347
- //#region src/react/conversation-search.tsx
356
+ //#region src/conversation-search.tsx
348
357
  function ConversationSearch({ value, onChange, className }) {
349
358
  return /* @__PURE__ */ jsx("div", {
350
359
  className: cn("border-b border-[#e9edef] shrink-0 p-2", className),
@@ -365,11 +374,46 @@ function ConversationSearch({ value, onChange, className }) {
365
374
  });
366
375
  }
367
376
  //#endregion
368
- //#region src/react/conversation-list.tsx
377
+ //#region src/conversation-filter-chips.tsx
378
+ const chips = [{
379
+ label: "Tudo",
380
+ value: "all"
381
+ }, {
382
+ label: "Não lidas",
383
+ value: "unread"
384
+ }];
385
+ function ConversationFilterChips({ value, onValueChange, unreadCount = 0, className }) {
386
+ return /* @__PURE__ */ jsx("div", {
387
+ className: cn("flex items-center gap-2 px-3 py-3", className),
388
+ children: chips.map((chip) => {
389
+ const isActive = chip.value === value;
390
+ const showCount = chip.value === "unread" && unreadCount > 0;
391
+ return /* @__PURE__ */ jsxs("button", {
392
+ type: "button",
393
+ onClick: () => onValueChange(chip.value),
394
+ "aria-pressed": isActive,
395
+ className: cn("inline-flex h-8 items-center rounded-full border px-4 text-[15px] font-medium transition-colors", isActive ? "border-[#b8e6c1] bg-[#e7fce3] text-[#017561]" : "border-[#d1d7db] bg-white text-[#54656f] hover:bg-[#f5f6f6]"),
396
+ children: [/* @__PURE__ */ jsx("span", { children: chip.label }), showCount ? /* @__PURE__ */ jsx("span", {
397
+ className: "ml-1",
398
+ children: unreadCount
399
+ }) : null]
400
+ }, chip.value);
401
+ })
402
+ });
403
+ }
404
+ //#endregion
405
+ //#region src/conversation-list.tsx
369
406
  function ConversationList({ conversations, isLoading, isError, selectedConversationId, onSelect, className }) {
370
407
  const { isMobile, mobileView, setMobileView } = useWhatsappDashboard();
371
408
  const [search, setSearch] = useState("");
372
- const filtered = conversations.filter((c) => c.phone.includes(search) || c.contactName?.toLowerCase().includes(search.toLowerCase()));
409
+ const [filter, setFilter] = useState("all");
410
+ const normalizedSearch = search.trim().toLowerCase();
411
+ const unreadConversationsCount = conversations.filter((c) => c.unreadCount > 0).length;
412
+ const filtered = conversations.filter((conversation) => {
413
+ const matchesSearch = normalizedSearch.length === 0 || conversation.phone.toLowerCase().includes(normalizedSearch) || conversation.contactName?.toLowerCase().includes(normalizedSearch);
414
+ const matchesFilter = filter === "all" || conversation.unreadCount > 0;
415
+ return matchesSearch && matchesFilter;
416
+ });
373
417
  const handleSelect = (id) => {
374
418
  onSelect(id);
375
419
  setMobileView("chat");
@@ -378,32 +422,53 @@ function ConversationList({ conversations, isLoading, isError, selectedConversat
378
422
  return /* @__PURE__ */ jsxs("div", {
379
423
  className: cn("flex flex-col h-full bg-white border-r border-[#e9edef]", isMobile ? "w-full" : "min-w-[320px] max-w-105", className),
380
424
  style: isVisible ? void 0 : { display: "none" },
381
- children: [/* @__PURE__ */ jsx(ConversationSearch, {
382
- value: search,
383
- onChange: setSearch
384
- }), /* @__PURE__ */ jsx("div", {
385
- className: "flex-1 overflow-y-auto overflow-x-hidden chat-scrollbar",
386
- children: isLoading ? /* @__PURE__ */ jsx("div", {
387
- className: "flex items-center justify-center h-full text-sm text-[#667781]",
388
- children: "Carregando..."
389
- }) : isError ? /* @__PURE__ */ jsx("div", {
390
- className: "flex items-center justify-center h-full text-sm text-red-500",
391
- children: "Erro ao carregar conversas"
392
- }) : filtered.length === 0 ? /* @__PURE__ */ jsxs("div", {
393
- className: "flex flex-col items-center justify-center h-full gap-2 text-[#667781]",
394
- children: [/* @__PURE__ */ jsx(HugeiconsIcon, {
395
- icon: Message01Icon,
396
- size: 32
397
- }), /* @__PURE__ */ jsx("p", {
398
- className: "text-sm",
399
- children: "Nenhuma conversa encontrada"
400
- })]
401
- }) : filtered.map((conversation) => /* @__PURE__ */ jsx(ConversationItem, {
402
- conversation,
403
- isSelected: selectedConversationId === conversation.id,
404
- onClick: () => handleSelect(conversation.id)
405
- }, conversation.id))
406
- })]
425
+ children: [
426
+ /* @__PURE__ */ jsx(ConversationSearch, {
427
+ value: search,
428
+ onChange: setSearch
429
+ }),
430
+ /* @__PURE__ */ jsx(ConversationFilterChips, {
431
+ value: filter,
432
+ onValueChange: setFilter,
433
+ unreadCount: unreadConversationsCount
434
+ }),
435
+ /* @__PURE__ */ jsx("div", {
436
+ className: "min-h-0 flex-1",
437
+ children: isLoading ? /* @__PURE__ */ jsx("div", {
438
+ className: "flex items-center justify-center h-full text-sm text-[#667781]",
439
+ children: "Carregando..."
440
+ }) : isError ? /* @__PURE__ */ jsx("div", {
441
+ className: "flex items-center justify-center h-full text-sm text-red-500",
442
+ children: "Erro ao carregar conversas"
443
+ }) : filtered.length === 0 ? /* @__PURE__ */ jsxs("div", {
444
+ className: "flex flex-col items-center justify-center h-full gap-2 text-[#667781]",
445
+ children: [/* @__PURE__ */ jsx(HugeiconsIcon, {
446
+ icon: Message01Icon,
447
+ size: 32
448
+ }), /* @__PURE__ */ jsx("p", {
449
+ className: "text-sm",
450
+ children: "Nenhuma conversa encontrada"
451
+ })]
452
+ }) : /* @__PURE__ */ jsx(LegendList, {
453
+ className: "chat-scrollbar",
454
+ data: filtered,
455
+ estimatedItemSize: 72,
456
+ extraData: selectedConversationId,
457
+ getFixedItemSize: () => 72,
458
+ keyExtractor: (conversation) => conversation.id,
459
+ recycleItems: true,
460
+ renderItem: ({ item: conversation }) => /* @__PURE__ */ jsx(ConversationItem, {
461
+ conversation,
462
+ isSelected: selectedConversationId === conversation.id,
463
+ onClick: () => handleSelect(conversation.id)
464
+ }),
465
+ style: {
466
+ height: "100%",
467
+ overflowX: "hidden"
468
+ }
469
+ })
470
+ })
471
+ ]
407
472
  });
408
473
  }
409
474
  function ConversationItem({ conversation, isSelected, onClick }) {
@@ -466,7 +531,7 @@ function formatTime(dateStr) {
466
531
  }
467
532
  }
468
533
  //#endregion
469
- //#region src/react/message-input.tsx
534
+ //#region src/message-input.tsx
470
535
  function MessageInput({ onSend, conversation, messages, disabled, placeholder = "Digite uma mensagem", className, contextWindowOpen = true }) {
471
536
  const [text, setText] = useState("");
472
537
  const [isSending, setIsSending] = useState(false);
@@ -581,4 +646,4 @@ function MessageInput({ onSend, conversation, messages, disabled, placeholder =
581
646
  });
582
647
  }
583
648
  //#endregion
584
- export { ConversationItem, ConversationList, FormattedMessage, MessageBubble, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, WhatsappDashboard, cn, useWhatsappDashboard };
649
+ export { ConversationFilterChips, ConversationItem, ConversationList, FormattedMessage, MessageBubble, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, WhatsappDashboard, cn, getDisplayDate, useWhatsappDashboard };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-zap/react",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "description": "React components for Better Zap.",
5
5
  "license": "ISC",
6
6
  "type": "module",
@@ -33,10 +33,11 @@
33
33
  "dependencies": {
34
34
  "@hugeicons/core-free-icons": "^3.1.1",
35
35
  "@hugeicons/react": "^1.1.4",
36
+ "@legendapp/list": "^3.1.1",
36
37
  "class-variance-authority": "^0.7.1",
37
38
  "clsx": "^2.1.1",
38
39
  "tailwind-merge": "^3.0.0",
39
- "better-zap": "0.0.3"
40
+ "better-zap": "0.1.0"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "react": "^19.0.0",