import classNames from 'classnames' import React from 'react' import type { BubbleGroupPosition, BubbleVariant } from '../types' export interface BubbleProps { variant: BubbleVariant /** Optional message text rendered below the attachment slot. */ text?: React.ReactNode /** * Renders a hairline border around the bubble. Defaults to `true` — * matches the design system's "small border around link / message * attachments" treatment from the mobile spec. */ bordered?: boolean /** * Position of this bubble inside a same-author message run. Drives * the corner-flattening that visually merges consecutive bubbles * (matches the grouping stream-chat-react applies to text bubbles). * Defaults to `'single'` so standalone usage keeps every corner * fully rounded. */ groupPosition?: BubbleGroupPosition /** * Width utility that REPLACES the default `'w-[280px]'`. Supply the * whole declaration (`'w-fit'`, `'w-[383px] max-w-full'`) — nothing * is merged with the default. */ widthClassName?: string /** * Padding utility that REPLACES the default `'px-2 py-2'`. Supply * the full inset (`'p-0.5'`, `'py-5 pl-5 pr-3'`). */ paddingClassName?: string /** * Corner utility that REPLACES the `groupPosition`-derived corner * table entirely. Opting in means opting OUT of same-author corner * flattening — the bubble renders the supplied radius on all four * corners. That is what the attachment redesign wants: Figma draws * media cards at a uniform r20 even when a caption bubble sits 4px * below (nodes `1973:12776`, `1973:20239`). * * It replaces rather than appends because appending cannot be made * reliable — an appended `rounded-[20px]` and the corner table have * identical CSS specificity, so Tailwind's emitted source order, * not argument order, would pick the winner. */ radiusClassName?: string className?: string children: React.ReactNode 'data-testid'?: string } // Colors and metrics tracked from `stream-chat-react`'s default // `.str-chat__message-bubble` token set so attachments visually drop // into a normal `CustomMessage` thread without any seams: // - light → `--str-chat__message-bubble-background-color` in `styles.css` (#f2f1ef) // - dark → `.str-chat__message--me` override in `styles.css` (#1e2330) // - text padding → `--str-chat__spacing-2 --str-chat__spacing-4` = 8px 16px // - border-radius → `--str-chat__border-radius-md` = 18px // // TODO: migrate to Tailwind theme tokens once they exist for the // `linktree.dark` / `linktree.surface.secondary` palette — these hex // literals are repeated across LinkAttachment / LockedAttachment / // MediaMessage / CustomMessageInput / DownloadAction etc., so a // rebrand would need to touch every site. A central token would // collapse the migration to one definition. const BUBBLE_BG_BY_VARIANT: Record = { dark: 'bg-[#1e2330]', light: 'bg-[#f2f1ef]', } const BUBBLE_TEXT_BY_VARIANT: Record = { dark: 'text-white', light: 'text-[#000000]', } const BUBBLE_BORDER_BY_VARIANT: Record = { dark: 'border-white/[0.08]', light: 'border-black/[0.08]', } /** * Per-corner rounding tables that mirror stream-chat-react's default * bubble grouping. Sender bubbles cluster against the right edge so * the right corners get flattened in a run; receiver bubbles cluster * against the left edge so the left corners get flattened. * * Rounded corners use `--str-chat__border-radius-md` (18px) and * flattened corners use `--str-chat__border-radius-sm` (4px) — same * tokens stream's stylesheet uses for `.str-chat__message-bubble` * inside a `--first` / `--group` / `--end` wrapper. */ type BubbleSide = 'sender' | 'receiver' const sideForVariant = (variant: BubbleVariant): BubbleSide => variant === 'dark' ? 'sender' : 'receiver' const CORNER_CLASSES_BY_SIDE_AND_POSITION: Record< BubbleSide, Record > = { sender: { single: 'rounded-tl-[18px] rounded-tr-[18px] rounded-bl-[18px] rounded-br-[18px]', first: 'rounded-tl-[18px] rounded-tr-[18px] rounded-bl-[18px] rounded-br-[4px]', middle: 'rounded-tl-[18px] rounded-tr-[4px] rounded-bl-[18px] rounded-br-[4px]', end: 'rounded-tl-[18px] rounded-tr-[4px] rounded-bl-[18px] rounded-br-[18px]', }, receiver: { single: 'rounded-tl-[18px] rounded-tr-[18px] rounded-bl-[18px] rounded-br-[18px]', first: 'rounded-tl-[18px] rounded-tr-[18px] rounded-bl-[4px] rounded-br-[18px]', middle: 'rounded-tl-[4px] rounded-tr-[18px] rounded-bl-[4px] rounded-br-[18px]', end: 'rounded-tl-[4px] rounded-tr-[18px] rounded-bl-[18px] rounded-br-[18px]', }, } /** * Chat-bubble container shared by every `MessageAttachment.*` card. * * Holds the attachment slot (image / video / row) and an optional * `text` caption rendered below it — mirrors the mobile chat layout * where a sender can attach a caption alongside an attachment ('Here * is the file', 'Here is the image'). * * Two visual variants: * - `dark` — sender-side (Composer / Sent), white text on `#1e2330`. * - `light` — recipient-side (Received), dark text on `#f2f1ef`. * * Background colors, padding, border-radius, and inherited font come * from the stream-chat-react `.str-chat__message-bubble` token set so * attachments line up with the `CustomMessage` text bubbles in a real * conversation. Padding is uniform across every variant — text rows, * audio rows, document rows, and media bubbles all get the same * 8px / 16px inset, with media inside getting its own rounded corners. */ const Bubble: React.FC = ({ variant, text, bordered = true, groupPosition = 'single', widthClassName = 'w-[280px]', paddingClassName = 'px-2 py-2', radiusClassName, className, children, 'data-testid': dataTestId, }) => { const hasText = text != null && text !== '' const cornerClasses = radiusClassName ?? CORNER_CLASSES_BY_SIDE_AND_POSITION[sideForVariant(variant)][groupPosition] return (
{children} {hasText ? (

{text}

) : null}
) } export default Bubble