@better-zap/react 0.2.2 → 25.0.1

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/README.md CHANGED
@@ -105,7 +105,7 @@ dependency, and Better Zap **adapters** that map domain data onto those primitiv
105
105
  | --- | --- | --- |
106
106
  | Data mapping (domain → presentation) | None — you pass `align`/`variant` directly | Own it — map `Conversation`/`UIMessage` fields (`sender`/`status`/`direction`) to presentational props |
107
107
  | Row layout | `Message`, `MessageAvatar`, `MessageContent`, `MessageHeader`, `MessageFooter` | Composed internally by `MessageBubble` / `MessageList`'s default renderer |
108
- | Bubble surface | `Bubble`, `BubbleContent`, `BubbleReactions` | Composed internally; still swappable via `renderMessage` (see [MessageList](#messagelist)) |
108
+ | Bubble surface | `Bubble`, `BubbleContent`, `BubbleReactions`, `LinkPreviewCard` | Composed internally; still swappable via `renderMessage` (see [MessageList](#messagelist)) |
109
109
  | Metadata / timestamps | You render them (`MessageFooter`, etc.) | Own default formatting (`formatDate`/`formatTime`), overridable via props |
110
110
  | Actions | `ComposerButton`, `ComposerSend`, etc. — you wire callbacks | `MessageInput` gates action buttons on callbacks being provided (see [Actions and failures](#actions-and-failures)) |
111
111
  | Responsive orchestration | None | `WhatsappDashboard` owns mobile list/chat navigation; `ConversationList`/`MessageView` read it optionally |
@@ -123,7 +123,8 @@ The root entry (`@better-zap/react`) re-exports the full public surface and is a
123
123
  | `@better-zap/react` | client | Full barrel |
124
124
  | `@better-zap/react/bubble` | server-safe | Presentational bubble primitives |
125
125
  | `@better-zap/react/message` | server-safe | Row layout primitives |
126
- | `@better-zap/react/message-bubble` | server-safe | Domain `MessageBubble` adapter |
126
+ | `@better-zap/react/message-bubble` | server-safe | Domain `MessageBubble` adapter (renders the client `LinkPreviewCard` when given a preview) |
127
+ | `@better-zap/react/link-preview-card` | client | Open Graph card (hides its image if it fails to load) |
127
128
  | `@better-zap/react/utils` | server-safe | `cn`, `getDisplayDate`, `renderSlot` |
128
129
  | `@better-zap/react/composer` | client | Draft/send orchestration |
129
130
  | `@better-zap/react/message-input` | client | Domain input + freeform window |
@@ -424,6 +425,85 @@ prepends *duplicate-label* groups (accepted).
424
425
  Also available: `formatTime(iso)` for the **default** bubble timestamp only
425
426
  (custom `renderMessage` owns its own time formatting).
426
427
 
428
+ ## Link previews
429
+
430
+ WhatsApp shows an Open Graph card (image, title, description, domain) at the
431
+ top of a bubble that contains a link. You supply the preview data; the
432
+ components only render it.
433
+
434
+ - `MessageList` `resolveLinkPreview(message)` returns a `LinkPreview` (or
435
+ `null`/`undefined`) for each message. The default renderer shows the card,
436
+ and custom `renderMessage` functions receive it as `linkPreview`.
437
+ - `MessageBubble` `linkPreview` renders the card inside the bubble, above the
438
+ text. The bubble takes WhatsApp's fixed 330px preview width (still capped at
439
+ 65% of the chat). Without a preview the bubble renders exactly as before.
440
+ - `LinkPreviewCard` is the card on its own, for `Bubble` compositions. Pass the
441
+ bubble's `variant` so the card gets the matching tint.
442
+
443
+ ```ts
444
+ interface LinkPreview {
445
+ url: string;
446
+ title: string;
447
+ description?: string;
448
+ image?: string; // absolute http(s) URL
449
+ siteName?: string;
450
+ }
451
+ ```
452
+
453
+ `resolveLinkPreview` is synchronous. Fetch previews elsewhere, keep them in
454
+ state, and pass a new resolver when they change: rows re-render when the
455
+ function's identity changes, so wrap it in `useCallback` keyed on that state.
456
+ `firstLinkIn` from `better-zap` picks the same link `FormattedMessage` renders
457
+ as an anchor:
458
+
459
+ ```tsx
460
+ import { useCallback } from "react";
461
+ import { MessageList } from "@better-zap/react";
462
+ import type { LinkPreview, UIMessage } from "@better-zap/react";
463
+ import { firstLinkIn } from "better-zap";
464
+
465
+ function ChatWithPreviews({
466
+ messages,
467
+ previews,
468
+ }: {
469
+ messages: UIMessage[];
470
+ previews: Record<string, LinkPreview>;
471
+ }) {
472
+ const resolveLinkPreview = useCallback(
473
+ (message: UIMessage) => {
474
+ const url = message.templateName ? null : firstLinkIn(message.content);
475
+ return url ? previews[url] : undefined;
476
+ },
477
+ [previews],
478
+ );
479
+
480
+ return (
481
+ <MessageList messages={messages} resolveLinkPreview={resolveLinkPreview} />
482
+ );
483
+ }
484
+ ```
485
+
486
+ In your own `Bubble` composition:
487
+
488
+ ```tsx
489
+ import { Bubble, BubbleContent, LinkPreviewCard } from "@better-zap/react";
490
+
491
+ <Bubble variant="primary" align="end" className="w-[330px]">
492
+ <BubbleContent>
493
+ <LinkPreviewCard preview={preview} variant="primary" />
494
+ {text}
495
+ </BubbleContent>
496
+ </Bubble>
497
+ ```
498
+
499
+ Fetching stays in your app because it needs SSRF guards, timeouts, size limits
500
+ and caching. Do it on the server, never from the browser. `better-zap` ships
501
+ `parseOpenGraph(html, pageUrl)` to turn the fetched page into a `LinkPreview`.
502
+ Some sites (Amazon, for example) return full Open Graph tags only to known
503
+ unfurler user agents such as `WhatsApp/2.23.20.0`. The
504
+ [link previews guide](https://github.com/Dosbodoke/better-zap/blob/main/landing/content/docs/guides/link-previews.mdx) has a
505
+ complete server fetcher and client hook.
506
+
427
507
  ## MessageView
428
508
 
429
509
  **MessageView** is the chat pane shell (background, empty state, header, content).
@@ -523,9 +603,5 @@ remain public — nothing described below removes an export.
523
603
 
524
604
  ### Versioning
525
605
 
526
- `@better-zap/react` is `0.x`; per SemVer, minor releases in this range may
527
- include breaking changes. This wave, however, removes **no** high-level
528
- export: `MessageBubble`, `MessageInput`, `MessageList`, `ConversationList`,
529
- `MessageView`, and `WhatsappDashboard` all remain public. The composable
530
- primitives (`Bubble*`, `Message*` layout parts, `Composer*`, `DateDivider`)
531
- are additive — they sit alongside the adapters, not in place of them.
606
+ See the [package release policy](../../README.md#releases) and the
607
+ [maintainer release runbook](../../.changeset/README.md).
package/dist/index.cjs CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_utils = require("./utils-Bp9IahGG.cjs");
4
4
  const require_whatsapp_dashboard = require("./whatsapp-dashboard.cjs");
5
5
  const require_bubble = require("./bubble.cjs");
6
+ const require_link_preview_card = require("./link-preview-card.cjs");
6
7
  const require_message = require("./message.cjs");
7
8
  const require_message_bubble = require("./message-bubble.cjs");
8
9
  const require_message_view = require("./message-view.cjs");
@@ -24,6 +25,7 @@ exports.ConversationList = require_conversation_list.ConversationList;
24
25
  exports.DateDivider = require_message_view.DateDivider;
25
26
  exports.FormattedMessage = require_message_bubble.FormattedMessage;
26
27
  exports.FreeformWindowClosedError = require_message_input.FreeformWindowClosedError;
28
+ exports.LinkPreviewCard = require_link_preview_card.LinkPreviewCard;
27
29
  exports.Message = require_message.Message;
28
30
  exports.MessageAvatar = require_message.MessageAvatar;
29
31
  exports.MessageBubble = require_message_bubble.MessageBubble;
package/dist/index.d.cts CHANGED
@@ -4,10 +4,11 @@ import { a as ConversationListProps, c as ConversationFilterValue, i as Conversa
4
4
  import { MobileView, WhatsappDashboard, WhatsappDashboardContextValue, WhatsappDashboardProps, useOptionalWhatsappDashboard, useWhatsappDashboard } from "./whatsapp-dashboard.cjs";
5
5
  import { DateDivider, DateDividerRenderContext, MessageGroupPosition, MessageList, MessageListProps, MessageRenderContext, MessageView, MessageViewContent, MessageViewContentProps, MessageViewEmpty, MessageViewHeader, MessageViewHeaderLabels, MessageViewHeaderProps, MessageViewProps } from "./message-view.cjs";
6
6
  import { FormattedMessage, MessageBubble, MessageBubbleGroupPosition, MessageBubbleProps } from "./message-bubble.cjs";
7
+ import { LinkPreviewCard, LinkPreviewCardProps } from "./link-preview-card.cjs";
7
8
  import { Message, MessageAlign, MessageAvatar, MessageAvatarProps, MessageContent, MessageContentProps, MessageFooter, MessageFooterProps, MessageGroup, MessageGroupProps, MessageHeader, MessageHeaderProps, MessageProps } from "./message.cjs";
8
9
  import { cn, getDisplayDate, renderSlot } from "./utils.cjs";
9
10
  import { FreeformWindowClosedError, MessageInput, MessageInputLabels, MessageInputProps } from "./message-input.cjs";
10
- import { Conversation, Conversation as Conversation$1, ConversationRecord, FreeformMessageWindow, FreeformMessageWindow as FreeformMessageWindow$1, UIMessage, UIMessage as UIMessage$1, UIMessageStatus } from "better-zap";
11
+ import { Conversation, Conversation as Conversation$1, ConversationRecord, FreeformMessageWindow, FreeformMessageWindow as FreeformMessageWindow$1, LinkPreview, UIMessage, UIMessage as UIMessage$1, UIMessageStatus } from "better-zap";
11
12
 
12
13
  //#region src/use-freeform-message-window.d.ts
13
14
  interface UseFreeformMessageWindowResult extends FreeformMessageWindow$1 {
@@ -16,4 +17,4 @@ interface UseFreeformMessageWindowResult extends FreeformMessageWindow$1 {
16
17
  }
17
18
  declare function useFreeformMessageWindow(conversation?: Pick<Conversation$1, "freeformMessageWindow" | "lastIncomingMessageAt"> | null, messages?: UIMessage$1[]): UseFreeformMessageWindowResult;
18
19
  //#endregion
19
- export { Bubble, BubbleAlign, BubbleContent, BubbleContentProps, BubbleGroup, BubbleGroupProps, BubbleProps, BubbleReactions, BubbleReactionsProps, BubbleVariant, Composer, ComposerButton, ComposerButtonProps, ComposerContextValue, ComposerError, ComposerErrorProps, ComposerProps, ComposerSend, ComposerSendProps, ComposerStateContextValue, ComposerTextarea, ComposerTextareaProps, type Conversation, ConversationFilterChips, ConversationFilterChipsProps, ConversationFilterValue, ConversationItem, ConversationItemProps, ConversationList, ConversationListLabels, ConversationListProps, type ConversationRecord, DateDivider, DateDividerRenderContext, FormattedMessage, type FreeformMessageWindow, FreeformWindowClosedError, Message, MessageAlign, MessageAvatar, MessageAvatarProps, MessageBubble, MessageBubbleGroupPosition, MessageBubbleProps, MessageContent, MessageContentProps, MessageFooter, MessageFooterProps, MessageGroup, MessageGroupPosition, MessageGroupProps, MessageHeader, MessageHeaderProps, MessageInput, MessageInputLabels, MessageInputProps, MessageList, MessageListProps, MessageProps, MessageRenderContext, MessageView, MessageViewContent, MessageViewContentProps, MessageViewEmpty, MessageViewHeader, MessageViewHeaderLabels, MessageViewHeaderProps, MessageViewProps, MobileView, type UIMessage, type UIMessageStatus, UseFreeformMessageWindowResult, WhatsappDashboard, WhatsappDashboardContextValue, WhatsappDashboardProps, cn, getDisplayDate, renderSlot, useComposer, useComposerState, useComposerValue, useFreeformMessageWindow, useOptionalWhatsappDashboard, useWhatsappDashboard };
20
+ export { Bubble, BubbleAlign, BubbleContent, BubbleContentProps, BubbleGroup, BubbleGroupProps, BubbleProps, BubbleReactions, BubbleReactionsProps, BubbleVariant, Composer, ComposerButton, ComposerButtonProps, ComposerContextValue, ComposerError, ComposerErrorProps, ComposerProps, ComposerSend, ComposerSendProps, ComposerStateContextValue, ComposerTextarea, ComposerTextareaProps, type Conversation, ConversationFilterChips, ConversationFilterChipsProps, ConversationFilterValue, ConversationItem, ConversationItemProps, ConversationList, ConversationListLabels, ConversationListProps, type ConversationRecord, DateDivider, DateDividerRenderContext, FormattedMessage, type FreeformMessageWindow, FreeformWindowClosedError, type LinkPreview, LinkPreviewCard, LinkPreviewCardProps, Message, MessageAlign, MessageAvatar, MessageAvatarProps, MessageBubble, MessageBubbleGroupPosition, MessageBubbleProps, MessageContent, MessageContentProps, MessageFooter, MessageFooterProps, MessageGroup, MessageGroupPosition, MessageGroupProps, MessageHeader, MessageHeaderProps, MessageInput, MessageInputLabels, MessageInputProps, MessageList, MessageListProps, MessageProps, MessageRenderContext, MessageView, MessageViewContent, MessageViewContentProps, MessageViewEmpty, MessageViewHeader, MessageViewHeaderLabels, MessageViewHeaderProps, MessageViewProps, MobileView, type UIMessage, type UIMessageStatus, UseFreeformMessageWindowResult, WhatsappDashboard, WhatsappDashboardContextValue, WhatsappDashboardProps, cn, getDisplayDate, renderSlot, useComposer, useComposerState, useComposerValue, useFreeformMessageWindow, useOptionalWhatsappDashboard, useWhatsappDashboard };
package/dist/index.d.mts CHANGED
@@ -4,10 +4,11 @@ import { a as ConversationListProps, c as ConversationFilterValue, i as Conversa
4
4
  import { MobileView, WhatsappDashboard, WhatsappDashboardContextValue, WhatsappDashboardProps, useOptionalWhatsappDashboard, useWhatsappDashboard } from "./whatsapp-dashboard.mjs";
5
5
  import { DateDivider, DateDividerRenderContext, MessageGroupPosition, MessageList, MessageListProps, MessageRenderContext, MessageView, MessageViewContent, MessageViewContentProps, MessageViewEmpty, MessageViewHeader, MessageViewHeaderLabels, MessageViewHeaderProps, MessageViewProps } from "./message-view.mjs";
6
6
  import { FormattedMessage, MessageBubble, MessageBubbleGroupPosition, MessageBubbleProps } from "./message-bubble.mjs";
7
+ import { LinkPreviewCard, LinkPreviewCardProps } from "./link-preview-card.mjs";
7
8
  import { Message, MessageAlign, MessageAvatar, MessageAvatarProps, MessageContent, MessageContentProps, MessageFooter, MessageFooterProps, MessageGroup, MessageGroupProps, MessageHeader, MessageHeaderProps, MessageProps } from "./message.mjs";
8
9
  import { cn, getDisplayDate, renderSlot } from "./utils.mjs";
9
10
  import { FreeformWindowClosedError, MessageInput, MessageInputLabels, MessageInputProps } from "./message-input.mjs";
10
- import { Conversation, Conversation as Conversation$1, ConversationRecord, FreeformMessageWindow, FreeformMessageWindow as FreeformMessageWindow$1, UIMessage, UIMessage as UIMessage$1, UIMessageStatus } from "better-zap";
11
+ import { Conversation, Conversation as Conversation$1, ConversationRecord, FreeformMessageWindow, FreeformMessageWindow as FreeformMessageWindow$1, LinkPreview, UIMessage, UIMessage as UIMessage$1, UIMessageStatus } from "better-zap";
11
12
 
12
13
  //#region src/use-freeform-message-window.d.ts
13
14
  interface UseFreeformMessageWindowResult extends FreeformMessageWindow$1 {
@@ -16,4 +17,4 @@ interface UseFreeformMessageWindowResult extends FreeformMessageWindow$1 {
16
17
  }
17
18
  declare function useFreeformMessageWindow(conversation?: Pick<Conversation$1, "freeformMessageWindow" | "lastIncomingMessageAt"> | null, messages?: UIMessage$1[]): UseFreeformMessageWindowResult;
18
19
  //#endregion
19
- export { Bubble, BubbleAlign, BubbleContent, BubbleContentProps, BubbleGroup, BubbleGroupProps, BubbleProps, BubbleReactions, BubbleReactionsProps, BubbleVariant, Composer, ComposerButton, ComposerButtonProps, ComposerContextValue, ComposerError, ComposerErrorProps, ComposerProps, ComposerSend, ComposerSendProps, ComposerStateContextValue, ComposerTextarea, ComposerTextareaProps, type Conversation, ConversationFilterChips, ConversationFilterChipsProps, ConversationFilterValue, ConversationItem, ConversationItemProps, ConversationList, ConversationListLabels, ConversationListProps, type ConversationRecord, DateDivider, DateDividerRenderContext, FormattedMessage, type FreeformMessageWindow, FreeformWindowClosedError, Message, MessageAlign, MessageAvatar, MessageAvatarProps, MessageBubble, MessageBubbleGroupPosition, MessageBubbleProps, MessageContent, MessageContentProps, MessageFooter, MessageFooterProps, MessageGroup, MessageGroupPosition, MessageGroupProps, MessageHeader, MessageHeaderProps, MessageInput, MessageInputLabels, MessageInputProps, MessageList, MessageListProps, MessageProps, MessageRenderContext, MessageView, MessageViewContent, MessageViewContentProps, MessageViewEmpty, MessageViewHeader, MessageViewHeaderLabels, MessageViewHeaderProps, MessageViewProps, MobileView, type UIMessage, type UIMessageStatus, UseFreeformMessageWindowResult, WhatsappDashboard, WhatsappDashboardContextValue, WhatsappDashboardProps, cn, getDisplayDate, renderSlot, useComposer, useComposerState, useComposerValue, useFreeformMessageWindow, useOptionalWhatsappDashboard, useWhatsappDashboard };
20
+ export { Bubble, BubbleAlign, BubbleContent, BubbleContentProps, BubbleGroup, BubbleGroupProps, BubbleProps, BubbleReactions, BubbleReactionsProps, BubbleVariant, Composer, ComposerButton, ComposerButtonProps, ComposerContextValue, ComposerError, ComposerErrorProps, ComposerProps, ComposerSend, ComposerSendProps, ComposerStateContextValue, ComposerTextarea, ComposerTextareaProps, type Conversation, ConversationFilterChips, ConversationFilterChipsProps, ConversationFilterValue, ConversationItem, ConversationItemProps, ConversationList, ConversationListLabels, ConversationListProps, type ConversationRecord, DateDivider, DateDividerRenderContext, FormattedMessage, type FreeformMessageWindow, FreeformWindowClosedError, type LinkPreview, LinkPreviewCard, LinkPreviewCardProps, Message, MessageAlign, MessageAvatar, MessageAvatarProps, MessageBubble, MessageBubbleGroupPosition, MessageBubbleProps, MessageContent, MessageContentProps, MessageFooter, MessageFooterProps, MessageGroup, MessageGroupPosition, MessageGroupProps, MessageHeader, MessageHeaderProps, MessageInput, MessageInputLabels, MessageInputProps, MessageList, MessageListProps, MessageProps, MessageRenderContext, MessageView, MessageViewContent, MessageViewContentProps, MessageViewEmpty, MessageViewHeader, MessageViewHeaderLabels, MessageViewHeaderProps, MessageViewProps, MobileView, type UIMessage, type UIMessageStatus, UseFreeformMessageWindowResult, WhatsappDashboard, WhatsappDashboardContextValue, WhatsappDashboardProps, cn, getDisplayDate, renderSlot, useComposer, useComposerState, useComposerValue, useFreeformMessageWindow, useOptionalWhatsappDashboard, useWhatsappDashboard };
package/dist/index.mjs CHANGED
@@ -2,10 +2,11 @@
2
2
  import { cn, getDisplayDate, renderSlot } from "./utils.mjs";
3
3
  import { WhatsappDashboard, useOptionalWhatsappDashboard, useWhatsappDashboard } from "./whatsapp-dashboard.mjs";
4
4
  import { Bubble, BubbleContent, BubbleGroup, BubbleReactions } from "./bubble.mjs";
5
+ import { LinkPreviewCard } from "./link-preview-card.mjs";
5
6
  import { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader } from "./message.mjs";
6
7
  import { FormattedMessage, MessageBubble } from "./message-bubble.mjs";
7
8
  import { DateDivider, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader } from "./message-view.mjs";
8
9
  import { n as ConversationList, r as ConversationFilterChips, t as ConversationItem } from "./conversation-list-C0EmReS-.mjs";
9
10
  import { Composer, ComposerButton, ComposerError, ComposerSend, ComposerTextarea, useComposer, useComposerState, useComposerValue } from "./composer.mjs";
10
11
  import { n as MessageInput, r as useFreeformMessageWindow, t as FreeformWindowClosedError } from "./message-input-CkaL6fb4.mjs";
11
- export { Bubble, BubbleContent, BubbleGroup, BubbleReactions, Composer, ComposerButton, ComposerError, ComposerSend, ComposerTextarea, ConversationFilterChips, ConversationItem, ConversationList, DateDivider, FormattedMessage, FreeformWindowClosedError, Message, MessageAvatar, MessageBubble, MessageContent, MessageFooter, MessageGroup, MessageHeader, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, WhatsappDashboard, cn, getDisplayDate, renderSlot, useComposer, useComposerState, useComposerValue, useFreeformMessageWindow, useOptionalWhatsappDashboard, useWhatsappDashboard };
12
+ export { Bubble, BubbleContent, BubbleGroup, BubbleReactions, Composer, ComposerButton, ComposerError, ComposerSend, ComposerTextarea, ConversationFilterChips, ConversationItem, ConversationList, DateDivider, FormattedMessage, FreeformWindowClosedError, LinkPreviewCard, Message, MessageAvatar, MessageBubble, MessageContent, MessageFooter, MessageGroup, MessageHeader, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, WhatsappDashboard, cn, getDisplayDate, renderSlot, useComposer, useComposerState, useComposerValue, useFreeformMessageWindow, useOptionalWhatsappDashboard, useWhatsappDashboard };
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_utils = require("./utils-Bp9IahGG.cjs");
4
+ let react = require("react");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
+ //#region src/link-preview-card.tsx
7
+ const tintByVariant = {
8
+ default: "bg-[#f0f2f5]",
9
+ primary: "bg-[#d1f4cc]",
10
+ destructive: "bg-[#f8cdc9]",
11
+ outline: "bg-black/5",
12
+ muted: "bg-black/5"
13
+ };
14
+ /**
15
+ * WhatsApp-style Open Graph card: image, two-line title, two-line description
16
+ * and the link's domain. Render it inside `BubbleContent`, above the text.
17
+ * The image is hidden if it fails to load.
18
+ */
19
+ function LinkPreviewCard({ preview, variant = "default", className, ...props }) {
20
+ const [failedImage, setFailedImage] = (0, react.useState)(null);
21
+ const image = preview.image && preview.image !== failedImage ? preview.image : null;
22
+ const domain = displayDomain(preview);
23
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
24
+ href: httpUrlOrUndefined(preview.url),
25
+ target: "_blank",
26
+ rel: "noopener noreferrer",
27
+ "data-slot": "link-preview",
28
+ className: require_utils.cn("-mx-[5px] -mt-[2px] mb-1.5 block overflow-hidden rounded-[6px] whitespace-normal no-underline focus-visible:outline-2 focus-visible:outline-offset-2", tintByVariant[variant], className),
29
+ ...props,
30
+ children: [image && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
31
+ src: image,
32
+ alt: "",
33
+ loading: "lazy",
34
+ decoding: "async",
35
+ referrerPolicy: "no-referrer",
36
+ className: "block max-h-[180px] w-full bg-white object-cover",
37
+ onError: () => setFailedImage(image),
38
+ ref: (element) => {
39
+ if (element?.complete && element.naturalWidth === 0) setFailedImage(image);
40
+ }
41
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
42
+ className: "block px-2.5 py-2",
43
+ children: [
44
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
45
+ className: "line-clamp-2 text-[13.5px] font-medium leading-[18px] text-[#111b21]",
46
+ children: preview.title
47
+ }),
48
+ preview.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49
+ className: "mt-0.5 line-clamp-2 text-[12.5px] leading-[17px] text-[#667781]",
50
+ children: preview.description
51
+ }),
52
+ domain && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53
+ className: "mt-1 block truncate text-[12px] leading-4 text-[#8696a0]",
54
+ children: domain
55
+ })
56
+ ]
57
+ })]
58
+ });
59
+ }
60
+ function parseUrl(value) {
61
+ try {
62
+ return new URL(value);
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ function httpUrlOrUndefined(value) {
68
+ const url = parseUrl(value);
69
+ return url && (url.protocol === "http:" || url.protocol === "https:") ? value : void 0;
70
+ }
71
+ function displayDomain(preview) {
72
+ const hostname = parseUrl(preview.url)?.hostname;
73
+ return hostname ? hostname.replace(/^www\./i, "") : preview.siteName;
74
+ }
75
+ //#endregion
76
+ exports.LinkPreviewCard = LinkPreviewCard;
@@ -0,0 +1,26 @@
1
+ import { BubbleVariant } from "./bubble.cjs";
2
+ import React from "react";
3
+ import { LinkPreview, LinkPreview as LinkPreview$1 } from "better-zap";
4
+
5
+ //#region src/link-preview-card.d.ts
6
+ interface LinkPreviewCardProps extends Omit<React.ComponentProps<"a">, "children"> {
7
+ preview: LinkPreview$1;
8
+ /**
9
+ * Variant of the surrounding `Bubble`, so the card gets the matching tint.
10
+ * @default "default"
11
+ */
12
+ variant?: BubbleVariant;
13
+ }
14
+ /**
15
+ * WhatsApp-style Open Graph card: image, two-line title, two-line description
16
+ * and the link's domain. Render it inside `BubbleContent`, above the text.
17
+ * The image is hidden if it fails to load.
18
+ */
19
+ declare function LinkPreviewCard({
20
+ preview,
21
+ variant,
22
+ className,
23
+ ...props
24
+ }: LinkPreviewCardProps): React.JSX.Element;
25
+ //#endregion
26
+ export { type LinkPreview, LinkPreviewCard, LinkPreviewCardProps };
@@ -0,0 +1,26 @@
1
+ import { BubbleVariant } from "./bubble.mjs";
2
+ import React from "react";
3
+ import { LinkPreview, LinkPreview as LinkPreview$1 } from "better-zap";
4
+
5
+ //#region src/link-preview-card.d.ts
6
+ interface LinkPreviewCardProps extends Omit<React.ComponentProps<"a">, "children"> {
7
+ preview: LinkPreview$1;
8
+ /**
9
+ * Variant of the surrounding `Bubble`, so the card gets the matching tint.
10
+ * @default "default"
11
+ */
12
+ variant?: BubbleVariant;
13
+ }
14
+ /**
15
+ * WhatsApp-style Open Graph card: image, two-line title, two-line description
16
+ * and the link's domain. Render it inside `BubbleContent`, above the text.
17
+ * The image is hidden if it fails to load.
18
+ */
19
+ declare function LinkPreviewCard({
20
+ preview,
21
+ variant,
22
+ className,
23
+ ...props
24
+ }: LinkPreviewCardProps): React.JSX.Element;
25
+ //#endregion
26
+ export { type LinkPreview, LinkPreviewCard, LinkPreviewCardProps };
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { cn } from "./utils.mjs";
3
+ import { useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/link-preview-card.tsx
6
+ const tintByVariant = {
7
+ default: "bg-[#f0f2f5]",
8
+ primary: "bg-[#d1f4cc]",
9
+ destructive: "bg-[#f8cdc9]",
10
+ outline: "bg-black/5",
11
+ muted: "bg-black/5"
12
+ };
13
+ /**
14
+ * WhatsApp-style Open Graph card: image, two-line title, two-line description
15
+ * and the link's domain. Render it inside `BubbleContent`, above the text.
16
+ * The image is hidden if it fails to load.
17
+ */
18
+ function LinkPreviewCard({ preview, variant = "default", className, ...props }) {
19
+ const [failedImage, setFailedImage] = useState(null);
20
+ const image = preview.image && preview.image !== failedImage ? preview.image : null;
21
+ const domain = displayDomain(preview);
22
+ return /* @__PURE__ */ jsxs("a", {
23
+ href: httpUrlOrUndefined(preview.url),
24
+ target: "_blank",
25
+ rel: "noopener noreferrer",
26
+ "data-slot": "link-preview",
27
+ className: cn("-mx-[5px] -mt-[2px] mb-1.5 block overflow-hidden rounded-[6px] whitespace-normal no-underline focus-visible:outline-2 focus-visible:outline-offset-2", tintByVariant[variant], className),
28
+ ...props,
29
+ children: [image && /* @__PURE__ */ jsx("img", {
30
+ src: image,
31
+ alt: "",
32
+ loading: "lazy",
33
+ decoding: "async",
34
+ referrerPolicy: "no-referrer",
35
+ className: "block max-h-[180px] w-full bg-white object-cover",
36
+ onError: () => setFailedImage(image),
37
+ ref: (element) => {
38
+ if (element?.complete && element.naturalWidth === 0) setFailedImage(image);
39
+ }
40
+ }), /* @__PURE__ */ jsxs("span", {
41
+ className: "block px-2.5 py-2",
42
+ children: [
43
+ /* @__PURE__ */ jsx("span", {
44
+ className: "line-clamp-2 text-[13.5px] font-medium leading-[18px] text-[#111b21]",
45
+ children: preview.title
46
+ }),
47
+ preview.description && /* @__PURE__ */ jsx("span", {
48
+ className: "mt-0.5 line-clamp-2 text-[12.5px] leading-[17px] text-[#667781]",
49
+ children: preview.description
50
+ }),
51
+ domain && /* @__PURE__ */ jsx("span", {
52
+ className: "mt-1 block truncate text-[12px] leading-4 text-[#8696a0]",
53
+ children: domain
54
+ })
55
+ ]
56
+ })]
57
+ });
58
+ }
59
+ function parseUrl(value) {
60
+ try {
61
+ return new URL(value);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function httpUrlOrUndefined(value) {
67
+ const url = parseUrl(value);
68
+ return url && (url.protocol === "http:" || url.protocol === "https:") ? value : void 0;
69
+ }
70
+ function displayDomain(preview) {
71
+ const hostname = parseUrl(preview.url)?.hostname;
72
+ return hostname ? hostname.replace(/^www\./i, "") : preview.siteName;
73
+ }
74
+ //#endregion
75
+ export { LinkPreviewCard };
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_utils = require("./utils-Bp9IahGG.cjs");
3
3
  const require_bubble = require("./bubble.cjs");
4
+ const require_link_preview_card = require("./link-preview-card.cjs");
4
5
  const require_message = require("./message.cjs");
5
6
  let react = require("react");
6
7
  react = require_utils.__toESM(react);
@@ -16,7 +17,7 @@ const statusVariants = (0, require("class-variance-authority").cva)("flex items-
16
17
  } },
17
18
  defaultVariants: { variant: "default" }
18
19
  });
19
- function MessageBubble({ content, sender, timestamp, status, templateName, label, groupPosition = "single", className, ...props }) {
20
+ function MessageBubble({ content, sender, timestamp, status, templateName, label, groupPosition = "single", linkPreview, className, ...props }) {
20
21
  const isIncoming = sender === "user";
21
22
  const isFailed = status === "failed";
22
23
  const align = sender === "user" ? "start" : "end";
@@ -34,6 +35,7 @@ function MessageBubble({ content, sender, timestamp, status, templateName, label
34
35
  variant,
35
36
  align,
36
37
  tail: hasTail,
38
+ className: linkPreview ? "w-[330px]" : void 0,
37
39
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_bubble.BubbleContent, { children: [
38
40
  label && !isIncoming && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
39
41
  className: "mb-1 block text-xs font-medium opacity-70",
@@ -49,6 +51,10 @@ function MessageBubble({ content, sender, timestamp, status, templateName, label
49
51
  }), templateName]
50
52
  })
51
53
  }),
54
+ linkPreview && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_link_preview_card.LinkPreviewCard, {
55
+ preview: linkPreview,
56
+ variant
57
+ }),
52
58
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormattedMessage, { text: displayContent }),
53
59
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54
60
  className: "float-right -mb-1 ml-2 mt-1.5 flex items-center justify-end gap-1 shrink-0 h-[15px] select-none",
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
- import { UIMessageStatus } from "better-zap";
3
+ import { LinkPreview, UIMessageStatus } from "better-zap";
4
4
 
5
5
  //#region src/message-bubble.d.ts
6
6
  type MessageBubbleGroupPosition = "single" | "first" | "middle" | "last";
@@ -17,6 +17,11 @@ interface MessageBubbleProps extends React.HTMLAttributes<HTMLDivElement> {
17
17
  * WhatsApp's grouping. @default "single"
18
18
  */
19
19
  groupPosition?: MessageBubbleGroupPosition;
20
+ /**
21
+ * Open Graph card shown inside the bubble, above the text. The bubble takes
22
+ * WhatsApp's fixed preview width while a card is shown.
23
+ */
24
+ linkPreview?: LinkPreview | null;
20
25
  }
21
26
  declare function MessageBubble({
22
27
  content,
@@ -26,6 +31,7 @@ declare function MessageBubble({
26
31
  templateName,
27
32
  label,
28
33
  groupPosition,
34
+ linkPreview,
29
35
  className,
30
36
  ...props
31
37
  }: MessageBubbleProps): react_jsx_runtime0.JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
- import { UIMessageStatus } from "better-zap";
3
+ import { LinkPreview, UIMessageStatus } from "better-zap";
4
4
 
5
5
  //#region src/message-bubble.d.ts
6
6
  type MessageBubbleGroupPosition = "single" | "first" | "middle" | "last";
@@ -17,6 +17,11 @@ interface MessageBubbleProps extends React.HTMLAttributes<HTMLDivElement> {
17
17
  * WhatsApp's grouping. @default "single"
18
18
  */
19
19
  groupPosition?: MessageBubbleGroupPosition;
20
+ /**
21
+ * Open Graph card shown inside the bubble, above the text. The bubble takes
22
+ * WhatsApp's fixed preview width while a card is shown.
23
+ */
24
+ linkPreview?: LinkPreview | null;
20
25
  }
21
26
  declare function MessageBubble({
22
27
  content,
@@ -26,6 +31,7 @@ declare function MessageBubble({
26
31
  templateName,
27
32
  label,
28
33
  groupPosition,
34
+ linkPreview,
29
35
  className,
30
36
  ...props
31
37
  }: MessageBubbleProps): react_jsx_runtime0.JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { cn } from "./utils.mjs";
2
2
  import { Bubble, BubbleContent } from "./bubble.mjs";
3
+ import { LinkPreviewCard } from "./link-preview-card.mjs";
3
4
  import { Message, MessageContent } from "./message.mjs";
4
5
  import React from "react";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -15,7 +16,7 @@ const statusVariants = cva("flex items-center leading-none", {
15
16
  } },
16
17
  defaultVariants: { variant: "default" }
17
18
  });
18
- function MessageBubble({ content, sender, timestamp, status, templateName, label, groupPosition = "single", className, ...props }) {
19
+ function MessageBubble({ content, sender, timestamp, status, templateName, label, groupPosition = "single", linkPreview, className, ...props }) {
19
20
  const isIncoming = sender === "user";
20
21
  const isFailed = status === "failed";
21
22
  const align = sender === "user" ? "start" : "end";
@@ -33,6 +34,7 @@ function MessageBubble({ content, sender, timestamp, status, templateName, label
33
34
  variant,
34
35
  align,
35
36
  tail: hasTail,
37
+ className: linkPreview ? "w-[330px]" : void 0,
36
38
  children: /* @__PURE__ */ jsxs(BubbleContent, { children: [
37
39
  label && !isIncoming && /* @__PURE__ */ jsx("span", {
38
40
  className: "mb-1 block text-xs font-medium opacity-70",
@@ -48,6 +50,10 @@ function MessageBubble({ content, sender, timestamp, status, templateName, label
48
50
  }), templateName]
49
51
  })
50
52
  }),
53
+ linkPreview && /* @__PURE__ */ jsx(LinkPreviewCard, {
54
+ preview: linkPreview,
55
+ variant
56
+ }),
51
57
  /* @__PURE__ */ jsx(FormattedMessage, { text: displayContent }),
52
58
  /* @__PURE__ */ jsxs("div", {
53
59
  className: "float-right -mb-1 ml-2 mt-1.5 flex items-center justify-end gap-1 shrink-0 h-[15px] select-none",
@@ -116,8 +116,19 @@ function MessageViewContent({ children, autoScroll = true, onScrollTop, classNam
116
116
  })
117
117
  });
118
118
  }
119
- const MemoizedMessageBubble = react.default.memo(require_message_bubble.MessageBubble);
119
+ const MemoizedMessageBubble = react.default.memo(require_message_bubble.MessageBubble, areMessageBubblePropsEqual);
120
120
  const MemoizedDateDivider = react.default.memo(DateDivider);
121
+ function areMessageBubblePropsEqual(prev, next) {
122
+ const { linkPreview: prevPreview, ...prevRest } = prev;
123
+ const { linkPreview: nextPreview, ...nextRest } = next;
124
+ return (prevPreview && nextPreview ? shallowEqual(prevPreview, nextPreview) : !prevPreview && !nextPreview) && shallowEqual(prevRest, nextRest);
125
+ }
126
+ function shallowEqual(a, b) {
127
+ const left = a;
128
+ const right = b;
129
+ const keys = Object.keys(left);
130
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.is(left[key], right[key]));
131
+ }
121
132
  function defaultFormatTime(isoDate) {
122
133
  return new Date(isoDate).toLocaleTimeString("pt-BR", {
123
134
  hour: "2-digit",
@@ -136,7 +147,7 @@ function getGroupPosition(messages, index, resolveFormatDate) {
136
147
  if (prevSame && nextSame) return "middle";
137
148
  return "last";
138
149
  }
139
- function MessageList({ messages, renderMessage, renderDateDivider, formatDate, formatTime, renderMessageLabel, autoScroll: autoScrollProp, onScrollTop: onScrollTopProp, className }) {
150
+ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, formatTime, renderMessageLabel, resolveLinkPreview, autoScroll: autoScrollProp, onScrollTop: onScrollTopProp, className }) {
140
151
  const scrollContext = (0, react.useContext)(MessageViewScrollContext);
141
152
  const resolvedAutoScroll = autoScrollProp ?? scrollContext?.autoScroll ?? true;
142
153
  const resolvedOnScrollTop = onScrollTopProp ?? scrollContext?.onScrollTop;
@@ -180,6 +191,7 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
180
191
  contentContainerStyle: { paddingBottom: 16 },
181
192
  data: items,
182
193
  estimatedItemSize: 72,
194
+ extraData: resolveLinkPreview,
183
195
  getItemType: (item) => item.type,
184
196
  initialScrollAtEnd: resolvedAutoScroll,
185
197
  keyExtractor: (item) => item.id,
@@ -195,6 +207,7 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
195
207
  }) ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedDateDivider, { children: item.label });
196
208
  const { message, groupPosition } = item;
197
209
  const label = renderMessageLabel?.(message);
210
+ const linkPreview = resolveLinkPreview?.(message) ?? void 0;
198
211
  const direction = message.direction;
199
212
  const align = direction === "incoming" ? "start" : "end";
200
213
  const ctx = {
@@ -203,7 +216,8 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
203
216
  direction,
204
217
  align,
205
218
  groupPosition,
206
- label
219
+ label,
220
+ linkPreview
207
221
  };
208
222
  return renderMessage?.(ctx) ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedMessageBubble, {
209
223
  content: message.content || "",
@@ -212,7 +226,8 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
212
226
  status: message.status,
213
227
  templateName: message.templateName || void 0,
214
228
  label,
215
- groupPosition
229
+ groupPosition,
230
+ linkPreview
216
231
  });
217
232
  },
218
233
  stickyHeaderIndices,
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
- import { Conversation, UIMessage } from "better-zap";
3
+ import { Conversation, LinkPreview, UIMessage } from "better-zap";
4
4
 
5
5
  //#region src/message-view.d.ts
6
6
  interface MessageViewProps extends React.ComponentProps<"div"> {
@@ -63,6 +63,8 @@ interface MessageRenderContext {
63
63
  groupPosition: MessageGroupPosition;
64
64
  /** Result of renderMessageLabel, when provided. */
65
65
  label?: string;
66
+ /** Result of resolveLinkPreview, when it returned a preview. */
67
+ linkPreview?: LinkPreview;
66
68
  }
67
69
  interface DateDividerRenderContext {
68
70
  /** Formatted label (output of formatDate / getDisplayDate). */
@@ -79,6 +81,13 @@ interface MessageListProps {
79
81
  /** Formats the in-bubble timestamp used by the DEFAULT renderer. Default: pt-BR HH:mm. */
80
82
  formatTime?: (isoDate: string) => string;
81
83
  renderMessageLabel?: (message: UIMessage) => string | undefined;
84
+ /**
85
+ * Returns the Open Graph card for a message, shown by the default renderer
86
+ * and passed to `renderMessage` as `linkPreview`. Must be synchronous: keep
87
+ * fetched previews in state and pass a new function (e.g. from
88
+ * `useCallback`) when they change; rows re-render when its identity changes.
89
+ */
90
+ resolveLinkPreview?: (message: UIMessage) => LinkPreview | null | undefined;
82
91
  /** Direct props — context from MessageViewContent still works; these win over it. */
83
92
  autoScroll?: boolean;
84
93
  onScrollTop?: () => void;
@@ -91,6 +100,7 @@ declare function MessageList({
91
100
  formatDate,
92
101
  formatTime,
93
102
  renderMessageLabel,
103
+ resolveLinkPreview,
94
104
  autoScroll: autoScrollProp,
95
105
  onScrollTop: onScrollTopProp,
96
106
  className
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
- import { Conversation, UIMessage } from "better-zap";
3
+ import { Conversation, LinkPreview, UIMessage } from "better-zap";
4
4
 
5
5
  //#region src/message-view.d.ts
6
6
  interface MessageViewProps extends React.ComponentProps<"div"> {
@@ -63,6 +63,8 @@ interface MessageRenderContext {
63
63
  groupPosition: MessageGroupPosition;
64
64
  /** Result of renderMessageLabel, when provided. */
65
65
  label?: string;
66
+ /** Result of resolveLinkPreview, when it returned a preview. */
67
+ linkPreview?: LinkPreview;
66
68
  }
67
69
  interface DateDividerRenderContext {
68
70
  /** Formatted label (output of formatDate / getDisplayDate). */
@@ -79,6 +81,13 @@ interface MessageListProps {
79
81
  /** Formats the in-bubble timestamp used by the DEFAULT renderer. Default: pt-BR HH:mm. */
80
82
  formatTime?: (isoDate: string) => string;
81
83
  renderMessageLabel?: (message: UIMessage) => string | undefined;
84
+ /**
85
+ * Returns the Open Graph card for a message, shown by the default renderer
86
+ * and passed to `renderMessage` as `linkPreview`. Must be synchronous: keep
87
+ * fetched previews in state and pass a new function (e.g. from
88
+ * `useCallback`) when they change; rows re-render when its identity changes.
89
+ */
90
+ resolveLinkPreview?: (message: UIMessage) => LinkPreview | null | undefined;
82
91
  /** Direct props — context from MessageViewContent still works; these win over it. */
83
92
  autoScroll?: boolean;
84
93
  onScrollTop?: () => void;
@@ -91,6 +100,7 @@ declare function MessageList({
91
100
  formatDate,
92
101
  formatTime,
93
102
  renderMessageLabel,
103
+ resolveLinkPreview,
94
104
  autoScroll: autoScrollProp,
95
105
  onScrollTop: onScrollTopProp,
96
106
  className
@@ -114,8 +114,19 @@ function MessageViewContent({ children, autoScroll = true, onScrollTop, classNam
114
114
  })
115
115
  });
116
116
  }
117
- const MemoizedMessageBubble = React.memo(MessageBubble);
117
+ const MemoizedMessageBubble = React.memo(MessageBubble, areMessageBubblePropsEqual);
118
118
  const MemoizedDateDivider = React.memo(DateDivider);
119
+ function areMessageBubblePropsEqual(prev, next) {
120
+ const { linkPreview: prevPreview, ...prevRest } = prev;
121
+ const { linkPreview: nextPreview, ...nextRest } = next;
122
+ return (prevPreview && nextPreview ? shallowEqual(prevPreview, nextPreview) : !prevPreview && !nextPreview) && shallowEqual(prevRest, nextRest);
123
+ }
124
+ function shallowEqual(a, b) {
125
+ const left = a;
126
+ const right = b;
127
+ const keys = Object.keys(left);
128
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.is(left[key], right[key]));
129
+ }
119
130
  function defaultFormatTime(isoDate) {
120
131
  return new Date(isoDate).toLocaleTimeString("pt-BR", {
121
132
  hour: "2-digit",
@@ -134,7 +145,7 @@ function getGroupPosition(messages, index, resolveFormatDate) {
134
145
  if (prevSame && nextSame) return "middle";
135
146
  return "last";
136
147
  }
137
- function MessageList({ messages, renderMessage, renderDateDivider, formatDate, formatTime, renderMessageLabel, autoScroll: autoScrollProp, onScrollTop: onScrollTopProp, className }) {
148
+ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, formatTime, renderMessageLabel, resolveLinkPreview, autoScroll: autoScrollProp, onScrollTop: onScrollTopProp, className }) {
138
149
  const scrollContext = useContext(MessageViewScrollContext);
139
150
  const resolvedAutoScroll = autoScrollProp ?? scrollContext?.autoScroll ?? true;
140
151
  const resolvedOnScrollTop = onScrollTopProp ?? scrollContext?.onScrollTop;
@@ -178,6 +189,7 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
178
189
  contentContainerStyle: { paddingBottom: 16 },
179
190
  data: items,
180
191
  estimatedItemSize: 72,
192
+ extraData: resolveLinkPreview,
181
193
  getItemType: (item) => item.type,
182
194
  initialScrollAtEnd: resolvedAutoScroll,
183
195
  keyExtractor: (item) => item.id,
@@ -193,6 +205,7 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
193
205
  }) ?? /* @__PURE__ */ jsx(MemoizedDateDivider, { children: item.label });
194
206
  const { message, groupPosition } = item;
195
207
  const label = renderMessageLabel?.(message);
208
+ const linkPreview = resolveLinkPreview?.(message) ?? void 0;
196
209
  const direction = message.direction;
197
210
  const align = direction === "incoming" ? "start" : "end";
198
211
  const ctx = {
@@ -201,7 +214,8 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
201
214
  direction,
202
215
  align,
203
216
  groupPosition,
204
- label
217
+ label,
218
+ linkPreview
205
219
  };
206
220
  return renderMessage?.(ctx) ?? /* @__PURE__ */ jsx(MemoizedMessageBubble, {
207
221
  content: message.content || "",
@@ -210,7 +224,8 @@ function MessageList({ messages, renderMessage, renderDateDivider, formatDate, f
210
224
  status: message.status,
211
225
  templateName: message.templateName || void 0,
212
226
  label,
213
- groupPosition
227
+ groupPosition,
228
+ linkPreview
214
229
  });
215
230
  },
216
231
  stickyHeaderIndices,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@better-zap/react",
3
- "version": "0.2.2",
3
+ "version": "25.0.1",
4
4
  "description": "React components for Better Zap.",
5
5
  "license": "ISC",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/Dosbodoke/better-zap",
8
+ "url": "git+https://github.com/Dosbodoke/better-zap.git",
9
9
  "directory": "packages/react"
10
10
  },
11
11
  "type": "module",
@@ -19,54 +19,114 @@
19
19
  ],
20
20
  "exports": {
21
21
  ".": {
22
- "types": "./dist/index.d.mts",
23
- "import": "./dist/index.mjs",
24
- "require": "./dist/index.cjs"
22
+ "import": {
23
+ "types": "./dist/index.d.mts",
24
+ "default": "./dist/index.mjs"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
25
30
  },
26
31
  "./bubble": {
27
- "types": "./dist/bubble.d.mts",
28
- "import": "./dist/bubble.mjs",
29
- "require": "./dist/bubble.cjs"
32
+ "import": {
33
+ "types": "./dist/bubble.d.mts",
34
+ "default": "./dist/bubble.mjs"
35
+ },
36
+ "require": {
37
+ "types": "./dist/bubble.d.cts",
38
+ "default": "./dist/bubble.cjs"
39
+ }
30
40
  },
31
41
  "./message": {
32
- "types": "./dist/message.d.mts",
33
- "import": "./dist/message.mjs",
34
- "require": "./dist/message.cjs"
42
+ "import": {
43
+ "types": "./dist/message.d.mts",
44
+ "default": "./dist/message.mjs"
45
+ },
46
+ "require": {
47
+ "types": "./dist/message.d.cts",
48
+ "default": "./dist/message.cjs"
49
+ }
35
50
  },
36
51
  "./message-bubble": {
37
- "types": "./dist/message-bubble.d.mts",
38
- "import": "./dist/message-bubble.mjs",
39
- "require": "./dist/message-bubble.cjs"
52
+ "import": {
53
+ "types": "./dist/message-bubble.d.mts",
54
+ "default": "./dist/message-bubble.mjs"
55
+ },
56
+ "require": {
57
+ "types": "./dist/message-bubble.d.cts",
58
+ "default": "./dist/message-bubble.cjs"
59
+ }
60
+ },
61
+ "./link-preview-card": {
62
+ "import": {
63
+ "types": "./dist/link-preview-card.d.mts",
64
+ "default": "./dist/link-preview-card.mjs"
65
+ },
66
+ "require": {
67
+ "types": "./dist/link-preview-card.d.cts",
68
+ "default": "./dist/link-preview-card.cjs"
69
+ }
40
70
  },
41
71
  "./composer": {
42
- "types": "./dist/composer.d.mts",
43
- "import": "./dist/composer.mjs",
44
- "require": "./dist/composer.cjs"
72
+ "import": {
73
+ "types": "./dist/composer.d.mts",
74
+ "default": "./dist/composer.mjs"
75
+ },
76
+ "require": {
77
+ "types": "./dist/composer.d.cts",
78
+ "default": "./dist/composer.cjs"
79
+ }
45
80
  },
46
81
  "./message-input": {
47
- "types": "./dist/message-input.d.mts",
48
- "import": "./dist/message-input.mjs",
49
- "require": "./dist/message-input.cjs"
82
+ "import": {
83
+ "types": "./dist/message-input.d.mts",
84
+ "default": "./dist/message-input.mjs"
85
+ },
86
+ "require": {
87
+ "types": "./dist/message-input.d.cts",
88
+ "default": "./dist/message-input.cjs"
89
+ }
50
90
  },
51
91
  "./message-view": {
52
- "types": "./dist/message-view.d.mts",
53
- "import": "./dist/message-view.mjs",
54
- "require": "./dist/message-view.cjs"
92
+ "import": {
93
+ "types": "./dist/message-view.d.mts",
94
+ "default": "./dist/message-view.mjs"
95
+ },
96
+ "require": {
97
+ "types": "./dist/message-view.d.cts",
98
+ "default": "./dist/message-view.cjs"
99
+ }
55
100
  },
56
101
  "./conversation-list": {
57
- "types": "./dist/conversation-list.d.mts",
58
- "import": "./dist/conversation-list.mjs",
59
- "require": "./dist/conversation-list.cjs"
102
+ "import": {
103
+ "types": "./dist/conversation-list.d.mts",
104
+ "default": "./dist/conversation-list.mjs"
105
+ },
106
+ "require": {
107
+ "types": "./dist/conversation-list.d.cts",
108
+ "default": "./dist/conversation-list.cjs"
109
+ }
60
110
  },
61
111
  "./whatsapp-dashboard": {
62
- "types": "./dist/whatsapp-dashboard.d.mts",
63
- "import": "./dist/whatsapp-dashboard.mjs",
64
- "require": "./dist/whatsapp-dashboard.cjs"
112
+ "import": {
113
+ "types": "./dist/whatsapp-dashboard.d.mts",
114
+ "default": "./dist/whatsapp-dashboard.mjs"
115
+ },
116
+ "require": {
117
+ "types": "./dist/whatsapp-dashboard.d.cts",
118
+ "default": "./dist/whatsapp-dashboard.cjs"
119
+ }
65
120
  },
66
121
  "./utils": {
67
- "types": "./dist/utils.d.mts",
68
- "import": "./dist/utils.mjs",
69
- "require": "./dist/utils.cjs"
122
+ "import": {
123
+ "types": "./dist/utils.d.mts",
124
+ "default": "./dist/utils.mjs"
125
+ },
126
+ "require": {
127
+ "types": "./dist/utils.d.cts",
128
+ "default": "./dist/utils.cjs"
129
+ }
70
130
  },
71
131
  "./tailwind.css": "./dist/tailwind.css",
72
132
  "./package.json": "./package.json"
@@ -78,7 +138,9 @@
78
138
  "node": ">=20"
79
139
  },
80
140
  "publishConfig": {
81
- "access": "public"
141
+ "access": "public",
142
+ "registry": "https://registry.npmjs.org/",
143
+ "provenance": true
82
144
  },
83
145
  "dependencies": {
84
146
  "@hugeicons/core-free-icons": "^3.1.1",
@@ -87,7 +149,7 @@
87
149
  "class-variance-authority": "^0.7.1",
88
150
  "clsx": "^2.1.1",
89
151
  "tailwind-merge": "^3.0.0",
90
- "better-zap": "0.2.0"
152
+ "better-zap": "25.0.1"
91
153
  },
92
154
  "peerDependencies": {
93
155
  "react": "^19.0.0",
@@ -106,6 +168,7 @@
106
168
  "scripts": {
107
169
  "build": "tsdown",
108
170
  "typecheck": "tsc --noEmit",
109
- "test": "pnpm run build && vitest run"
171
+ "test": "pnpm run build && vitest run",
172
+ "package:check": "publint --strict && attw --pack --profile node16 --exclude-entrypoints @better-zap/react/tailwind.css"
110
173
  }
111
174
  }