{"version":3,"file":"ChatBubble.cjs","names":[],"sources":["../../../src/components/Chat/ChatBubble.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines props-count function-lines — one bubble is one unit of\n * layout, and every part of it reads from the same message: the quote above the\n * body, the body or its tombstone, the attachments, the reactions under it, and the\n * timestamp with its ticks. Splitting them would pass the same message to six\n * components and put the bubble's geometry in none of them.\n */\nimport { Suspense, lazy, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport type { ContextMenuItem } from \"../ContextMenu\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { chatStrings, timeLabel, type ChatMessage } from \"./chat-groups\";\nimport { receiptLabel, resolveReceiptState } from \"./chat-receipt\";\nimport styles from \"./Chat.module.css\";\n\nconst ChatAttachments = lazy(() =>\n    import(\"./ChatAttachments\").then((module) => ({ default: module.ChatAttachments })),\n);\n\nconst ChatActionsMenu = lazy(() =>\n    import(\"./ChatActionsMenu\").then((module) => ({ default: module.ChatActionsMenu })),\n);\n\n/** Glyph per tick state. `delivered` is the one a boolean `status` cannot say. */\nconst RECEIPT_GLYPH = {\n    sending: \"◌\",\n    sent: \"✓\",\n    delivered: \"✓✓\",\n    read: \"✓✓\",\n    failed: \"!\",\n} as const;\n\nexport interface ChatBubbleProps {\n    /** The message this bubble renders. */\n    message: ChatMessage;\n    /** Whether it belongs to the current user — side, colour and ticks. */\n    own: boolean;\n    /** Locale for labels. */\n    locale: \"pt-BR\" | \"en\";\n    /** Retry handler for a `\"failed\"` message. */\n    onRetry?: (message: ChatMessage) => void;\n    /** Toggles a reaction. Receives the emoji that was pressed. */\n    onReact?: (message: ChatMessage, emoji: string) => void;\n    /** Per-message actions, opened by right click, long press or the ⋮ button. */\n    messageActions?: (message: ChatMessage) => ContextMenuItem[];\n    /** Called when the quote stub is activated — scroll to the quoted message. */\n    onQuoteClick?: (message: ChatMessage) => void;\n}\n\n/**\n * One message bubble.\n *\n * Everything a messenger needs on a single message lives here, because every one\n * of these was a thing each app rebuilt on top of a text-only bubble: the reply\n * stub, attachments, reactions, per-recipient ticks, the actions menu, and the\n * tombstone of a deleted message.\n *\n * @param props - The message and its handlers.\n * @returns The bubble, wrapped in a context menu when actions are given.\n */\nexport function ChatBubble({\n    message,\n    own,\n    locale,\n    onRetry,\n    onReact,\n    messageActions,\n    onQuoteClick,\n}: ChatBubbleProps) {\n    const strings = chatStrings(locale);\n    const receiptState = own ? resolveReceiptState(message) : null;\n    const actions = message.deleted ? [] : (messageActions?.(message) ?? []);\n\n    const bubble = (\n        <div\n            className={cn(\n                styles.bubble,\n                own && styles.ownBubble,\n                message.deleted && styles.deletedBubble,\n                message.status === \"failed\" && styles.failedBubble,\n            )}\n        >\n            {message.quote && !message.deleted && (\n                <QuoteStub message={message} locale={locale} onQuoteClick={onQuoteClick} />\n            )}\n\n            {message.deleted ? (\n                <div className={styles.tombstone}>\n                    <span aria-hidden=\"true\">🚫</span> {strings.deleted}\n                </div>\n            ) : (\n                <>\n                    {message.attachments && message.attachments.length > 0 && (\n                        <Suspense fallback={<div className={styles.attachmentsFallback} />}>\n                            <ChatAttachments attachments={message.attachments} strings={strings} />\n                        </Suspense>\n                    )}\n                    {message.body !== undefined && message.body !== null && message.body !== \"\" && (\n                        <div className={styles.body}>{message.body}</div>\n                    )}\n                </>\n            )}\n\n            <div className={styles.meta}>\n                {message.edited && !message.deleted && (\n                    <span className={styles.edited}>{strings.edited}</span>\n                )}\n                <time dateTime={new Date(message.sentAt).toISOString()}>\n                    {timeLabel(message.sentAt, locale)}\n                </time>\n                {receiptState && <Receipt message={message} state={receiptState} locale={locale} />}\n            </div>\n        </div>\n    );\n\n    return (\n        <li className={styles.bubbleRow}>\n            {actions.length > 0 ? (\n                <Suspense fallback={bubble}>\n                    <ChatActionsMenu items={actions} trigger=\"contextmenu\">\n                        {bubble}\n                    </ChatActionsMenu>\n                </Suspense>\n            ) : (\n                bubble\n            )}\n\n            {actions.length > 0 && (\n                <Suspense fallback={null}>\n                    <ChatActionsMenu items={actions} trigger=\"click\">\n                        <button\n                            type=\"button\"\n                            className={styles.actionsButton}\n                            aria-label={strings.messageActions}\n                        >\n                            <span aria-hidden=\"true\">⋮</span>\n                        </button>\n                    </ChatActionsMenu>\n                </Suspense>\n            )}\n\n            {message.reactions && message.reactions.length > 0 && (\n                <ul className={styles.reactions} aria-label={strings.reactions}>\n                    {message.reactions.map((reaction) => (\n                        <li key={reaction.emoji}>\n                            <button\n                                type=\"button\"\n                                className={cn(\n                                    styles.reaction,\n                                    reaction.reacted && styles.reactionMine,\n                                )}\n                                aria-pressed={reaction.reacted ?? false}\n                                title={reaction.names?.join(\", \")}\n                                disabled={!onReact}\n                                onClick={() => onReact?.(message, reaction.emoji)}\n                            >\n                                <span aria-hidden=\"true\">\n                                    {reaction.emoji} {reaction.count}\n                                </span>\n                                <VisuallyHidden>\n                                    {strings.react(reaction.emoji, reaction.count)}\n                                </VisuallyHidden>\n                            </button>\n                        </li>\n                    ))}\n                </ul>\n            )}\n\n            {message.status === \"failed\" && onRetry && (\n                <button type=\"button\" className={styles.retry} onClick={() => onRetry(message)}>\n                    {strings.retry}\n                </button>\n            )}\n        </li>\n    );\n}\n\n/** The reply stub above a body, or its blanked form once the target is gone. */\nfunction QuoteStub({\n    message,\n    locale,\n    onQuoteClick,\n}: {\n    message: ChatMessage;\n    locale: \"pt-BR\" | \"en\";\n    onQuoteClick?: (message: ChatMessage) => void;\n}): ReactNode {\n    const strings = chatStrings(locale);\n    const quote = message.quote;\n    if (!quote) return null;\n    const label = quote.revoked\n        ? strings.quoteRevoked\n        : (quote.excerpt ??\n          (quote.kind && quote.kind !== \"text\" ? strings.quoteKind[quote.kind] : \"\"));\n\n    const content = (\n        <>\n            <span className={styles.quoteAuthor}>\n                {quote.senderName ?? strings.replyingTo(quote.messageId)}\n            </span>\n            <span className={cn(styles.quoteText, quote.revoked && styles.quoteRevoked)}>\n                {label}\n            </span>\n        </>\n    );\n\n    return onQuoteClick ? (\n        <button\n            type=\"button\"\n            className={cn(styles.quote, styles.quoteButton)}\n            onClick={() => onQuoteClick(message)}\n        >\n            {content}\n        </button>\n    ) : (\n        <div className={styles.quote}>{content}</div>\n    );\n}\n\n/** The ticks, and the sentence behind them. */\nfunction Receipt({\n    message,\n    state,\n    locale,\n}: {\n    message: ChatMessage;\n    state: keyof typeof RECEIPT_GLYPH;\n    locale: \"pt-BR\" | \"en\";\n}): ReactNode {\n    const strings = chatStrings(locale);\n    const label =\n        message.receipt && message.receipt.totalRecipients > 0 && state !== \"failed\"\n            ? receiptLabel(message.receipt, strings)\n            : strings[state === \"delivered\" ? \"sent\" : state];\n\n    return (\n        <span\n            className={cn(\n                styles.status,\n                state === \"read\" && styles.statusRead,\n                state === \"failed\" && styles.statusFailed,\n            )}\n            title={label}\n        >\n            <span aria-hidden=\"true\">{RECEIPT_GLYPH[state]}</span>\n            <VisuallyHidden>{label}</VisuallyHidden>\n        </span>\n    );\n}\n"],"mappings":"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"}