@better-zap/react 0.0.4 → 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,6 +28,7 @@ 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
34
  //#region src/utils.ts
@@ -256,70 +257,80 @@ function MessageViewHeader({ conversation, onBack, onInfoClick, className }) {
256
257
  })]
257
258
  });
258
259
  }
259
- const SCROLL_TOP_THRESHOLD = 50;
260
+ const MessageViewScrollContext = react.default.createContext(null);
260
261
  function MessageViewContent({ children, autoScroll = true, onScrollTop, className, ...props }) {
261
- const scrollRef = (0, react.useRef)(null);
262
- const prevScrollHeightRef = (0, react.useRef)(0);
263
- (0, react.useEffect)(() => {
264
- if (autoScroll && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
265
- }, [children, autoScroll]);
266
- (0, react.useEffect)(() => {
267
- const el = scrollRef.current;
268
- if (!el) return;
269
- const newScrollHeight = el.scrollHeight;
270
- const prevScrollHeight = prevScrollHeightRef.current;
271
- if (prevScrollHeight > 0 && newScrollHeight > prevScrollHeight) {
272
- const addedHeight = newScrollHeight - prevScrollHeight;
273
- if (el.scrollTop < SCROLL_TOP_THRESHOLD + addedHeight) el.scrollTop = addedHeight;
274
- }
275
- prevScrollHeightRef.current = newScrollHeight;
276
- });
277
- const handleScroll = (0, react.useCallback)(() => {
278
- const el = scrollRef.current;
279
- if (!el || !onScrollTop) return;
280
- if (el.scrollTop <= SCROLL_TOP_THRESHOLD) onScrollTop();
281
- }, [onScrollTop]);
282
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
283
- ref: scrollRef,
284
- className: cn("flex flex-1 flex-col overflow-y-auto p-4 pb-0 chat-scrollbar", className),
285
- onScroll: handleScroll,
286
- ...props,
287
- 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
+ })
288
272
  });
289
273
  }
290
274
  function MessageList({ messages, renderMessageLabel, className }) {
291
- const messageGroups = (0, react.useMemo)(() => {
292
- const groups = [];
293
- 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;
294
282
  messages.forEach((msg) => {
295
283
  const displayDate = getDisplayDate(msg.sentAt);
296
- if (!currentGroup || currentGroup.date !== displayDate) {
297
- currentGroup = {
298
- date: displayDate,
299
- messages: []
300
- };
301
- 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
+ });
302
292
  }
303
- currentGroup.messages.push(msg);
293
+ itemsNew.push({
294
+ type: "message",
295
+ id: msg.id,
296
+ message: msg
297
+ });
304
298
  });
305
- return groups;
299
+ return {
300
+ items: itemsNew,
301
+ stickyHeaderIndices: stickyHeaderIndicesNew
302
+ };
306
303
  }, [messages]);
307
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
308
- className: cn("flex flex-col pb-4", className),
309
- children: messageGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
310
- className: "flex flex-col gap-1",
311
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DateDivider, { date: group.date }), group.messages.map((msg) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, {
312
- content: msg.content || "",
313
- sender: msg.direction === "incoming" ? "user" : "bot",
314
- timestamp: new Date(msg.sentAt).toLocaleTimeString("pt-BR", {
315
- hour: "2-digit",
316
- minute: "2-digit"
317
- }),
318
- status: msg.status,
319
- templateName: msg.templateName || void 0,
320
- label: renderMessageLabel?.(msg)
321
- }, msg.id))]
322
- }, 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
+ }
323
334
  });
324
335
  }
325
336
  function DateDivider({ date }) {
@@ -446,7 +457,7 @@ function ConversationList({ conversations, isLoading, isError, selectedConversat
446
457
  unreadCount: unreadConversationsCount
447
458
  }),
448
459
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
449
- className: "flex-1 overflow-y-auto overflow-x-hidden chat-scrollbar",
460
+ className: "min-h-0 flex-1",
450
461
  children: isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
451
462
  className: "flex items-center justify-center h-full text-sm text-[#667781]",
452
463
  children: "Carregando..."
@@ -462,11 +473,24 @@ function ConversationList({ conversations, isLoading, isError, selectedConversat
462
473
  className: "text-sm",
463
474
  children: "Nenhuma conversa encontrada"
464
475
  })]
465
- }) : filtered.map((conversation) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationItem, {
466
- conversation,
467
- isSelected: selectedConversationId === conversation.id,
468
- onClick: () => handleSelect(conversation.id)
469
- }, conversation.id))
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
+ })
470
494
  })
471
495
  ]
472
496
  });
package/dist/index.mjs CHANGED
@@ -4,6 +4,7 @@ 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
10
  //#region src/utils.ts
@@ -232,70 +233,80 @@ function MessageViewHeader({ conversation, onBack, onInfoClick, className }) {
232
233
  })]
233
234
  });
234
235
  }
235
- const SCROLL_TOP_THRESHOLD = 50;
236
+ const MessageViewScrollContext = React.createContext(null);
236
237
  function MessageViewContent({ children, autoScroll = true, onScrollTop, className, ...props }) {
237
- const scrollRef = useRef(null);
238
- const prevScrollHeightRef = useRef(0);
239
- useEffect(() => {
240
- if (autoScroll && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
241
- }, [children, autoScroll]);
242
- useEffect(() => {
243
- const el = scrollRef.current;
244
- if (!el) return;
245
- const newScrollHeight = el.scrollHeight;
246
- const prevScrollHeight = prevScrollHeightRef.current;
247
- if (prevScrollHeight > 0 && newScrollHeight > prevScrollHeight) {
248
- const addedHeight = newScrollHeight - prevScrollHeight;
249
- if (el.scrollTop < SCROLL_TOP_THRESHOLD + addedHeight) el.scrollTop = addedHeight;
250
- }
251
- prevScrollHeightRef.current = newScrollHeight;
252
- });
253
- const handleScroll = useCallback(() => {
254
- const el = scrollRef.current;
255
- if (!el || !onScrollTop) return;
256
- if (el.scrollTop <= SCROLL_TOP_THRESHOLD) onScrollTop();
257
- }, [onScrollTop]);
258
- return /* @__PURE__ */ jsx("div", {
259
- ref: scrollRef,
260
- className: cn("flex flex-1 flex-col overflow-y-auto p-4 pb-0 chat-scrollbar", className),
261
- onScroll: handleScroll,
262
- ...props,
263
- 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
+ })
264
248
  });
265
249
  }
266
250
  function MessageList({ messages, renderMessageLabel, className }) {
267
- const messageGroups = useMemo(() => {
268
- const groups = [];
269
- 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;
270
258
  messages.forEach((msg) => {
271
259
  const displayDate = getDisplayDate(msg.sentAt);
272
- if (!currentGroup || currentGroup.date !== displayDate) {
273
- currentGroup = {
274
- date: displayDate,
275
- messages: []
276
- };
277
- 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
+ });
278
268
  }
279
- currentGroup.messages.push(msg);
269
+ itemsNew.push({
270
+ type: "message",
271
+ id: msg.id,
272
+ message: msg
273
+ });
280
274
  });
281
- return groups;
275
+ return {
276
+ items: itemsNew,
277
+ stickyHeaderIndices: stickyHeaderIndicesNew
278
+ };
282
279
  }, [messages]);
283
- return /* @__PURE__ */ jsx("div", {
284
- className: cn("flex flex-col pb-4", className),
285
- children: messageGroups.map((group) => /* @__PURE__ */ jsxs("div", {
286
- className: "flex flex-col gap-1",
287
- children: [/* @__PURE__ */ jsx(DateDivider, { date: group.date }), group.messages.map((msg) => /* @__PURE__ */ jsx(MessageBubble, {
288
- content: msg.content || "",
289
- sender: msg.direction === "incoming" ? "user" : "bot",
290
- timestamp: new Date(msg.sentAt).toLocaleTimeString("pt-BR", {
291
- hour: "2-digit",
292
- minute: "2-digit"
293
- }),
294
- status: msg.status,
295
- templateName: msg.templateName || void 0,
296
- label: renderMessageLabel?.(msg)
297
- }, msg.id))]
298
- }, 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
+ }
299
310
  });
300
311
  }
301
312
  function DateDivider({ date }) {
@@ -422,7 +433,7 @@ function ConversationList({ conversations, isLoading, isError, selectedConversat
422
433
  unreadCount: unreadConversationsCount
423
434
  }),
424
435
  /* @__PURE__ */ jsx("div", {
425
- className: "flex-1 overflow-y-auto overflow-x-hidden chat-scrollbar",
436
+ className: "min-h-0 flex-1",
426
437
  children: isLoading ? /* @__PURE__ */ jsx("div", {
427
438
  className: "flex items-center justify-center h-full text-sm text-[#667781]",
428
439
  children: "Carregando..."
@@ -438,11 +449,24 @@ function ConversationList({ conversations, isLoading, isError, selectedConversat
438
449
  className: "text-sm",
439
450
  children: "Nenhuma conversa encontrada"
440
451
  })]
441
- }) : filtered.map((conversation) => /* @__PURE__ */ jsx(ConversationItem, {
442
- conversation,
443
- isSelected: selectedConversationId === conversation.id,
444
- onClick: () => handleSelect(conversation.id)
445
- }, conversation.id))
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
+ })
446
470
  })
447
471
  ]
448
472
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-zap/react",
3
- "version": "0.0.4",
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.4"
40
+ "better-zap": "0.1.0"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "react": "^19.0.0",