@ttt-productions/chat-react 0.12.9 → 0.12.11

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ChatShell.js","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { ChatMessageV1, SendAttachmentFn, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport type {\n ChatCoreConfig,\n ChatAttachmentConfig,\n MessageRendererRegistry,\n} from \"../types.js\";\nimport { Card, CardHeader, CardContent, CardFooter, Skeleton } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n // Header\n header?: React.ReactNode;\n\n // Send handler — text-only. On the realtime transport this is OPTIONAL: the\n // shell sends through the DO socket. If provided, it is still called (so a\n // consumer can mirror to analytics), but the socket send is authoritative.\n onSend?: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n // Message rendering\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n // Composer attachment config\n attachmentConfig?: ChatAttachmentConfig;\n sendAttachment?: SendAttachmentFn;\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n // Three render slots\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n /** If provided, replaces the Composer entirely */\n renderFooter?: () => React.ReactNode;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n // Disable\n composerDisabled?: boolean;\n\n /** Fill the parent's height instead of the default fixed-height card. Lays the Card out\n * as `flex flex-col h-full` with the message list flexing to fill (scrolls inside), so a\n * full-height page panel doesn't grow unbounded or waste space on a fixed 400px box.\n * The consumer must give ChatShell a bounded-height parent (e.g. an `h-full` panel). */\n fillHeight?: boolean;\n /** Advanced override for the scrollable region's class (forwarded to MessageList).\n * Usually unnecessary — prefer `fillHeight`. */\n scrollClassName?: string;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n /** Retry a failed realtime send by its original clientMessageId (realtime only;\n * absent on firestore). Wired to MessageItemDefault's failed-bubble retry. */\n retrySend?: (clientMessageId: string) => void;\n /** Advance the authoritative read cursor (realtime only; absent on the firestore path).\n * Returns whether the ack was actually sent so the caller advances its local cursor only on success (M2). */\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n /** Realtime connection status — drives the disconnected banner (R13). Absent on firestore. */\n status?: string;\n /** uids currently typing (realtime only) — drives the typing indicator (R14). */\n typing?: string[];\n /** Signal the local user is typing (coalesced by the transport). Wired to the Composer (R14). */\n signalTyping?: () => void;\n};\n\n/**\n * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the\n * two transports live in separate inner components (each calls exactly ONE data\n * hook) and both render the shared presentational `ChatShellView`. The default\n * (undefined / 'firestore') transport is byte-for-byte the previous behavior.\n */\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === \"realtime\") {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n if (onSend) await onSend(text, replyTo);\n },\n [onSend],\n );\n return <ChatShellView {...props} resolved={{ ...r, send }} />;\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n // Map the UI replyTo (messageId-based) to the wire replyTo (seq + preview).\n const wireReply =\n replyTo && replyTo.messageId\n ? { messageSeq: Number(replyTo.messageId), preview: replyTo.messagePreview ?? \"\" }\n : null;\n const ok = r.send(text, Number.isFinite(wireReply?.messageSeq) ? wireReply : null);\n // C-B8: surface a closed-socket failure so the Composer keeps the user's text\n // instead of clearing it on a no-op send.\n if (!ok) throw new Error(\"chat-send-failed\");\n if (onSend) await onSend(text, replyTo); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n attachmentConfig,\n sendAttachment,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, send, retrySend, readAck, status, typing, signalTyping } = resolved;\n\n const [showScrollToBottom, setShowScrollToBottom] = React.useState(false);\n const [atBottom, setAtBottom] = React.useState(true);\n const [focused, setFocused] = React.useState(true);\n const lastAckedRef = React.useRef(0);\n const leaseHeldRef = React.useRef(false);\n\n // The latest authoritative seq currently rendered (optimistic rows have no seq).\n const latestSeq = React.useMemo(() => {\n let max = 0;\n for (const m of messages) {\n const seq = typeof m.meta?.seq === \"number\" ? (m.meta?.seq as number) : 0;\n if (seq > max) max = seq;\n }\n return max;\n }, [messages]);\n\n // Track tab focus/visibility so the read-ack \"focused\" lease releases on blur/hide.\n React.useEffect(() => {\n const compute = () =>\n typeof document === \"undefined\" ||\n (document.visibilityState !== \"hidden\" && (typeof document.hasFocus !== \"function\" || document.hasFocus()));\n setFocused(compute());\n const onChange = () => setFocused(compute());\n window.addEventListener(\"focus\", onChange);\n window.addEventListener(\"blur\", onChange);\n document.addEventListener(\"visibilitychange\", onChange);\n return () => {\n window.removeEventListener(\"focus\", onChange);\n window.removeEventListener(\"blur\", onChange);\n document.removeEventListener(\"visibilitychange\", onChange);\n };\n }, []);\n\n // C-B6: advance the authoritative read cursor when the reader is actually at the\n // latest message (channel mounted + tab focused + scrolled to bottom); release the\n // focus lease exactly once when they stop. No-op on the firestore path (no readAck).\n React.useEffect(() => {\n if (!readAck || latestSeq <= 0) return;\n if (focused && atBottom) {\n leaseHeldRef.current = true;\n if (latestSeq > lastAckedRef.current) {\n // Advance the local marker ONLY if the frame was actually sent — a drop during a\n // disconnect window must stay un-acked so it retries (the client also re-sends the\n // cursor on reconnect). (M2)\n if (readAck(latestSeq, true)) lastAckedRef.current = latestSeq;\n }\n } else if (leaseHeldRef.current) {\n leaseHeldRef.current = false;\n readAck(latestSeq, false);\n }\n }, [readAck, latestSeq, atBottom, focused]);\n\n // In fill mode the Card flexes to its parent's height and the message list fills the\n // space between header and composer; otherwise it's the default natural-height card.\n const cardClassName = fillHeight ? \"w-full flex flex-col h-full min-h-0\" : \"w-full\";\n const contentClassName = fillHeight ? \"p-0 flex-1 min-h-0 flex flex-col\" : \"p-0\";\n\n if (!allowed) {\n return <div className=\"p-4 text-sm opacity-70\">You don&apos;t have access to this thread.</div>;\n }\n\n if (isInitialLoading) {\n // Before the first authoritative snapshot/history result, show an HONEST working\n // state — a visible \"Opening chat…\" spinner in a polite live region — NOT a blank\n // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner\n // (which must not compete with the initial-opening message). Only the MessageList\n // region is replaced: the consumer's header + actions + the three render-slots +\n // footer all still render, because a consumer that hangs critical controls off those\n // slots (e.g. the guild-invite binding-agreement Agree/Decline/stake-share controls)\n // must work whether or not realtime chat has connected yet (F4.1).\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n <CardContent className={contentClassName}>\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-5 w-5 animate-spin\" aria-hidden=\"true\" />\n <span>Opening chat…</span>\n </div>\n </CardContent>\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t\">\n <Skeleton className=\"h-10 w-full\" />\n </CardFooter>\n )}\n </Card>\n );\n }\n\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n\n {/* R13: a disconnected/reconnecting banner so a configured-but-unreachable Worker\n never presents a healthy-looking empty thread. Only the realtime path sets `status`. */}\n <ChatConnectionBanner status={status} />\n\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n\n <CardContent className={contentClassName}>\n <MessageList\n messages={messages}\n currentUserId={config.currentUserId}\n isAdmin={config.isAdmin}\n isFetchingOlder={isFetchingOlder}\n hasOlder={hasOlder}\n onLoadOlder={() => fetchOlder()}\n renderMessage={renderMessage}\n messageRenderers={messageRenderers}\n showScrollToBottom={showScrollToBottom}\n onScrollToBottom={() => setShowScrollToBottom(false)}\n onAtBottomChange={setAtBottom}\n fillHeight={fillHeight}\n scrollClassName={scrollClassName}\n handlers={handlers}\n onSenderClick={onSenderClick}\n onRetrySend={retrySend}\n />\n </CardContent>\n\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n\n {renderFooter ? (\n <CardFooter className=\"border-t\">\n {renderFooter()}\n </CardFooter>\n ) : (\n <CardFooter className=\"border-t flex-col items-stretch gap-1\">\n {/* R14: typing dots above the composer (realtime only — `typing` is undefined on firestore). */}\n <TypingIndicator typing={typing} />\n <KeyboardAvoidingView padding offset={8} className=\"w-full\">\n <Composer\n onSend={send}\n onTyping={signalTyping}\n attachmentConfig={attachmentConfig}\n sendAttachment={sendAttachment}\n disabled={composerDisabled}\n autoFocus={autoFocus}\n placeholder={composerPlaceholder}\n mentionConfig={config.mentionConfig}\n />\n </KeyboardAvoidingView>\n </CardFooter>\n )}\n </Card>\n );\n}\n\n/**\n * R13 — connection-state banner. The realtime path sets `status`; once the first\n * snapshot has loaded (ChatShellView is past `isInitialLoading`), a dropped/unreachable\n * socket surfaces here instead of a silently-empty thread. Renders nothing on the\n * firestore path (`status` undefined) or while live (`open`).\n */\nfunction ChatConnectionBanner({ status }: { status?: string }) {\n if (status !== \"reconnecting\" && status !== \"closed\") return null;\n return (\n <div\n className=\"px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {status === \"reconnecting\"\n ? \"Reconnecting… messages may be delayed.\"\n : \"Disconnected — messages may be out of date.\"}\n </div>\n );\n}\n\n/** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport\n * excludes the local user). Generic copy (no name resolution) for v1. */\nfunction TypingIndicator({ typing }: { typing?: string[] }) {\n if (!typing || typing.length === 0) return null;\n return (\n <div className=\"text-xs italic text-muted-foreground\" aria-live=\"polite\">\n {typing.length === 1 ? \"Someone is typing…\" : \"Several people are typing…\"}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"ChatShell.js","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { ChatMessageV1, SendAttachmentFn, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport type {\n ChatCoreConfig,\n ChatAttachmentConfig,\n MessageRendererRegistry,\n} from \"../types.js\";\nimport { Card, CardHeader, CardContent, CardFooter, Skeleton } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport type { ComposerHandle } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n // Header\n header?: React.ReactNode;\n\n // Send handler — text-only. On the realtime transport this is OPTIONAL: the\n // shell sends through the DO socket. If provided, it is still called (so a\n // consumer can mirror to analytics), but the socket send is authoritative.\n onSend?: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n // Message rendering\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n // Composer attachment config\n attachmentConfig?: ChatAttachmentConfig;\n sendAttachment?: SendAttachmentFn;\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n // Three render slots\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n /** If provided, replaces the Composer entirely */\n renderFooter?: () => React.ReactNode;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n // Disable\n composerDisabled?: boolean;\n\n /** Fill the parent's height instead of the default fixed-height card. Lays the Card out\n * as `flex flex-col h-full` with the message list flexing to fill (scrolls inside), so a\n * full-height page panel doesn't grow unbounded or waste space on a fixed 400px box.\n * The consumer must give ChatShell a bounded-height parent (e.g. an `h-full` panel). */\n fillHeight?: boolean;\n /** Advanced override for the scrollable region's class (forwarded to MessageList).\n * Usually unnecessary — prefer `fillHeight`. */\n scrollClassName?: string;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n /** Retry a failed realtime send by its original clientMessageId (realtime only;\n * absent on firestore). Wired to MessageItemDefault's failed-bubble retry. */\n retrySend?: (clientMessageId: string) => void;\n /** Advance the authoritative read cursor (realtime only; absent on the firestore path).\n * Returns whether the ack was actually sent so the caller advances its local cursor only on success (M2). */\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n /** Realtime connection status — drives the disconnected banner (R13). Absent on firestore. */\n status?: string;\n /** uids currently typing (realtime only) — drives the typing indicator (R14). */\n typing?: string[];\n /** Signal the local user is typing (coalesced by the transport). Wired to the Composer (R14). */\n signalTyping?: () => void;\n};\n\n/**\n * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the\n * two transports live in separate inner components (each calls exactly ONE data\n * hook) and both render the shared presentational `ChatShellView`. The default\n * (undefined / 'firestore') transport is byte-for-byte the previous behavior.\n */\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === \"realtime\") {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n if (onSend) await onSend(text, replyTo);\n },\n [onSend],\n );\n return <ChatShellView {...props} resolved={{ ...r, send }} />;\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n // Map the UI replyTo (messageId-based) to the wire replyTo (seq + preview).\n const wireReply =\n replyTo && replyTo.messageId\n ? { messageSeq: Number(replyTo.messageId), preview: replyTo.messagePreview ?? \"\" }\n : null;\n const ok = r.send(text, Number.isFinite(wireReply?.messageSeq) ? wireReply : null);\n // C-B8: surface a closed-socket failure so the Composer keeps the user's text\n // instead of clearing it on a no-op send.\n if (!ok) throw new Error(\"chat-send-failed\");\n if (onSend) await onSend(text, replyTo); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n attachmentConfig,\n sendAttachment,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, send, retrySend, readAck, status, typing, signalTyping } = resolved;\n\n const [showScrollToBottom, setShowScrollToBottom] = React.useState(false);\n const [atBottom, setAtBottom] = React.useState(true);\n const [focused, setFocused] = React.useState(true);\n const lastAckedRef = React.useRef(0);\n const leaseHeldRef = React.useRef(false);\n\n // A terminally-failed attachment bubble re-opens the canonical picker through the\n // Composer's imperative handle. The action is offered only when attachment support\n // is actually configured AND the default Composer is rendered (a custom renderFooter\n // replaces it, so the picker would not exist). The new file then rides the ordinary\n // Composer → guarded-upload flow with a fresh id — never reusing a storage path or\n // mutating the terminal row.\n const composerRef = React.useRef<ComposerHandle>(null);\n const attachmentSupported = Boolean(attachmentConfig && sendAttachment && !renderFooter);\n const handleRetryAttachment = React.useCallback(() => {\n composerRef.current?.openAttachmentSelector();\n }, []);\n\n // The latest authoritative seq currently rendered (optimistic rows have no seq).\n const latestSeq = React.useMemo(() => {\n let max = 0;\n for (const m of messages) {\n const seq = typeof m.meta?.seq === \"number\" ? (m.meta?.seq as number) : 0;\n if (seq > max) max = seq;\n }\n return max;\n }, [messages]);\n\n // Track tab focus/visibility so the read-ack \"focused\" lease releases on blur/hide.\n React.useEffect(() => {\n const compute = () =>\n typeof document === \"undefined\" ||\n (document.visibilityState !== \"hidden\" && (typeof document.hasFocus !== \"function\" || document.hasFocus()));\n setFocused(compute());\n const onChange = () => setFocused(compute());\n window.addEventListener(\"focus\", onChange);\n window.addEventListener(\"blur\", onChange);\n document.addEventListener(\"visibilitychange\", onChange);\n return () => {\n window.removeEventListener(\"focus\", onChange);\n window.removeEventListener(\"blur\", onChange);\n document.removeEventListener(\"visibilitychange\", onChange);\n };\n }, []);\n\n // C-B6: advance the authoritative read cursor when the reader is actually at the\n // latest message (channel mounted + tab focused + scrolled to bottom); release the\n // focus lease exactly once when they stop. No-op on the firestore path (no readAck).\n React.useEffect(() => {\n if (!readAck || latestSeq <= 0) return;\n if (focused && atBottom) {\n leaseHeldRef.current = true;\n if (latestSeq > lastAckedRef.current) {\n // Advance the local marker ONLY if the frame was actually sent — a drop during a\n // disconnect window must stay un-acked so it retries (the client also re-sends the\n // cursor on reconnect). (M2)\n if (readAck(latestSeq, true)) lastAckedRef.current = latestSeq;\n }\n } else if (leaseHeldRef.current) {\n leaseHeldRef.current = false;\n readAck(latestSeq, false);\n }\n }, [readAck, latestSeq, atBottom, focused]);\n\n // In fill mode the Card flexes to its parent's height and the message list fills the\n // space between header and composer; otherwise it's the default natural-height card.\n const cardClassName = fillHeight ? \"w-full flex flex-col h-full min-h-0\" : \"w-full\";\n const contentClassName = fillHeight ? \"p-0 flex-1 min-h-0 flex flex-col\" : \"p-0\";\n\n if (!allowed) {\n return <div className=\"p-4 text-sm opacity-70\">You don&apos;t have access to this thread.</div>;\n }\n\n if (isInitialLoading) {\n // Before the first authoritative snapshot/history result, show an HONEST working\n // state — a visible \"Opening chat…\" spinner in a polite live region — NOT a blank\n // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner\n // (which must not compete with the initial-opening message). Only the MessageList\n // region is replaced: the consumer's header + actions + the three render-slots +\n // footer all still render, because a consumer that hangs critical controls off those\n // slots (e.g. the guild-invite binding-agreement Agree/Decline/stake-share controls)\n // must work whether or not realtime chat has connected yet (F4.1).\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n <CardContent className={contentClassName}>\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-5 w-5 animate-spin\" aria-hidden=\"true\" />\n <span>Opening chat…</span>\n </div>\n </CardContent>\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t\">\n <Skeleton className=\"h-10 w-full\" />\n </CardFooter>\n )}\n </Card>\n );\n }\n\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n\n {/* R13: a disconnected/reconnecting banner so a configured-but-unreachable Worker\n never presents a healthy-looking empty thread. Only the realtime path sets `status`. */}\n <ChatConnectionBanner status={status} />\n\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n\n <CardContent className={contentClassName}>\n <MessageList\n messages={messages}\n currentUserId={config.currentUserId}\n isAdmin={config.isAdmin}\n isFetchingOlder={isFetchingOlder}\n hasOlder={hasOlder}\n onLoadOlder={() => fetchOlder()}\n renderMessage={renderMessage}\n messageRenderers={messageRenderers}\n showScrollToBottom={showScrollToBottom}\n onScrollToBottom={() => setShowScrollToBottom(false)}\n onAtBottomChange={setAtBottom}\n fillHeight={fillHeight}\n scrollClassName={scrollClassName}\n handlers={handlers}\n onSenderClick={onSenderClick}\n onRetrySend={retrySend}\n onRetryAttachment={attachmentSupported ? handleRetryAttachment : undefined}\n />\n </CardContent>\n\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n\n {renderFooter ? (\n <CardFooter className=\"border-t\">\n {renderFooter()}\n </CardFooter>\n ) : (\n <CardFooter className=\"border-t flex-col items-stretch gap-1\">\n {/* R14: typing dots above the composer (realtime only — `typing` is undefined on firestore). */}\n <TypingIndicator typing={typing} />\n <KeyboardAvoidingView padding offset={8} className=\"w-full\">\n <Composer\n ref={composerRef}\n onSend={send}\n onTyping={signalTyping}\n attachmentConfig={attachmentConfig}\n sendAttachment={sendAttachment}\n disabled={composerDisabled}\n autoFocus={autoFocus}\n placeholder={composerPlaceholder}\n mentionConfig={config.mentionConfig}\n />\n </KeyboardAvoidingView>\n </CardFooter>\n )}\n </Card>\n );\n}\n\n/**\n * R13 — connection-state banner. The realtime path sets `status`; once the first\n * snapshot has loaded (ChatShellView is past `isInitialLoading`), a dropped/unreachable\n * socket surfaces here instead of a silently-empty thread. Renders nothing on the\n * firestore path (`status` undefined) or while live (`open`).\n */\nfunction ChatConnectionBanner({ status }: { status?: string }) {\n if (status !== \"reconnecting\" && status !== \"closed\") return null;\n return (\n <div\n className=\"px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {status === \"reconnecting\"\n ? \"Reconnecting… messages may be delayed.\"\n : \"Disconnected — messages may be out of date.\"}\n </div>\n );\n}\n\n/** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport\n * excludes the local user). Generic copy (no name resolution) for v1. */\nfunction TypingIndicator({ typing }: { typing?: string[] }) {\n if (!typing || typing.length === 0) return null;\n return (\n <div className=\"text-xs italic text-muted-foreground\" aria-live=\"polite\">\n {typing.length === 1 ? \"Someone is typing…\" : \"Several people are typing…\"}\n </div>\n );\n}\n"]}
@@ -27,5 +27,19 @@ export type ComposerProps = {
27
27
  placeholder?: string;
28
28
  mentionConfig?: import('../types.js').ChatMentionConfig;
29
29
  };
30
- export declare function Composer(props: ComposerProps): React.JSX.Element;
30
+ /**
31
+ * Imperative handle exposed by {@link Composer} via `ref`. Additive — ref-less
32
+ * usage is unaffected. ChatShell holds this ref so a failed-attachment bubble's
33
+ * "Attach again" action can re-open the canonical picker.
34
+ */
35
+ export type ComposerHandle = {
36
+ /**
37
+ * Open the attachment picker, delegating to the underlying MediaInput's
38
+ * `openSelection()`. Must be called synchronously from a user gesture. No-op
39
+ * when attachments are not configured (the MediaInput is not mounted) or the
40
+ * composer is disabled/sending (MediaInput's own gate no-ops it).
41
+ */
42
+ openAttachmentSelector: () => void;
43
+ };
44
+ export declare const Composer: React.ForwardRefExoticComponent<ComposerProps & React.RefAttributes<ComposerHandle>>;
31
45
  //# sourceMappingURL=Composer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/B,OAAO,KAAK,EAAkB,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAiCxD,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5E;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAGtB,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAElC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,aAAa,EAAE,iBAAiB,CAAC;CACzD,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,qBA6N5C"}
1
+ {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/B,OAAO,KAAK,EAAkB,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAiCxD,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5E;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAGtB,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAElC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,aAAa,EAAE,iBAAiB,CAAC;CACzD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;OAKG;IACH,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,QAAQ,sFAyOnB,CAAC"}
@@ -38,7 +38,7 @@ function getSelectedAttachmentLabel(type) {
38
38
  default: return "File selected";
39
39
  }
40
40
  }
41
- export function Composer(props) {
41
+ export const Composer = React.forwardRef(function Composer(props, forwardedRef) {
42
42
  const { onSend, onTyping, attachmentConfig, sendAttachment, disabled, autoFocus = false, placeholder = "Type a message...", } = props;
43
43
  const [text, setText] = React.useState("");
44
44
  const [pendingFile, setPendingFile] = React.useState(null);
@@ -47,6 +47,7 @@ export function Composer(props) {
47
47
  const [sendError, setSendError] = React.useState(null);
48
48
  const ref = React.useRef(null);
49
49
  const abortControllerRef = React.useRef(null);
50
+ const mediaInputRef = React.useRef(null);
50
51
  const guardedUpload = useGuardedUpload();
51
52
  // Navigation guard for the in-flight send: a send that has left the composer
52
53
  // but not yet committed is killable by navigation/sign-out with zero warning
@@ -83,6 +84,15 @@ export function Composer(props) {
83
84
  const handleCancelUpload = React.useCallback(() => {
84
85
  abortControllerRef.current?.abort();
85
86
  }, []);
87
+ // Additive imperative handle: re-open the canonical attachment picker. Delegates
88
+ // straight to MediaInput's openSelection so onBeforeSelect / disabled / validation
89
+ // / crop are all honored (and the disabled/sending gate is MediaInput's — it
90
+ // receives `disabled`/`isLoading` below). No-op when attachments aren't configured
91
+ // (MediaInput is unmounted → ref is null).
92
+ const openAttachmentSelector = React.useCallback(() => {
93
+ mediaInputRef.current?.openSelection();
94
+ }, []);
95
+ React.useImperativeHandle(forwardedRef, () => ({ openAttachmentSelector }), [openAttachmentSelector]);
86
96
  const send = async () => {
87
97
  const v = text.trim();
88
98
  if (!v && !pendingFile)
@@ -167,6 +177,6 @@ export function Composer(props) {
167
177
  e.preventDefault();
168
178
  send();
169
179
  }
170
- } }), _jsx(Button, { type: "button", variant: "default", disabled: isDisabled || (!text.trim() && !pendingFile), onClick: send, children: isSending ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : "Send" })] }), attachEnabled && attachmentConfig && (_jsx("div", { className: "mt-2", children: _jsx(MediaInput, { spec: attachmentConfig.attachmentSpec, onChange: handleFileSelected, selectedFile: pendingFile, isLoading: isSending, uploadState: uploadState, disabled: isDisabled, buttonLabel: "Attach file", onClear: () => setPendingFile(null), onCancel: handleCancelUpload, className: "w-full" }) }))] }));
171
- }
180
+ } }), _jsx(Button, { type: "button", variant: "default", disabled: isDisabled || (!text.trim() && !pendingFile), onClick: send, children: isSending ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : "Send" })] }), attachEnabled && attachmentConfig && (_jsx("div", { className: "mt-2", children: _jsx(MediaInput, { ref: mediaInputRef, spec: attachmentConfig.attachmentSpec, onChange: handleFileSelected, selectedFile: pendingFile, isLoading: isSending, uploadState: uploadState, disabled: isDisabled, buttonLabel: "Attach file", onClear: () => setPendingFile(null), onCancel: handleCancelUpload, className: "w-full" }) }))] }));
181
+ });
172
182
  //# sourceMappingURL=Composer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.js","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button, Textarea } from \"@ttt-productions/ui-core/react\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { ensureFileWithContentType } from \"@ttt-productions/file-input\";\nimport { MediaInput } from \"@ttt-productions/file-input/react\";\nimport type { MediaInputChangePayload } from \"@ttt-productions/file-input\";\nimport type { UploadState } from \"@ttt-productions/media-schemas\";\nimport { useGuardedUpload } from \"@ttt-productions/upload-ui/react/upload\";\nimport { useOptionalLocalUploadGuard } from \"@ttt-productions/upload-ui/react/guard\";\nimport { Loader2, X, FileText, ImageIcon, VideoIcon, MicIcon } from \"lucide-react\";\nimport type { ChatAttachment, ChatMessageV1, SendAttachmentFn } from \"@ttt-productions/chat-core\";\nimport type { ChatAttachmentConfig } from \"../types.js\";\nimport { useMentionAutocomplete } from \"../mentions/use-mention-autocomplete.js\";\nimport { MentionAutocomplete } from \"../mentions/MentionAutocomplete.js\";\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nfunction getAttachmentType(file: File): ChatAttachment[\"type\"] {\n if (file.type.startsWith(\"image/\")) return \"image\";\n if (file.type.startsWith(\"video/\")) return \"video\";\n if (file.type.startsWith(\"audio/\")) return \"audio\";\n return \"text\";\n}\n\nfunction AttachmentTypeIcon({ type }: { type: ChatAttachment[\"type\"] }) {\n switch (type) {\n case \"image\": return <ImageIcon className=\"h-4 w-4 shrink-0\" />;\n case \"video\": return <VideoIcon className=\"h-4 w-4 shrink-0\" />;\n case \"audio\": return <MicIcon className=\"h-4 w-4 shrink-0\" />;\n default: return <FileText className=\"h-4 w-4 shrink-0\" />;\n }\n}\n\nfunction getSelectedAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image selected\";\n case \"video\": return \"Video selected\";\n case \"audio\": return \"Audio selected\";\n default: return \"File selected\";\n }\n}\n\nexport type ComposerProps = {\n /**\n * Text-only send. Composer calls this when there is no pending attachment.\n * Consumer creates the message doc. Return value is ignored.\n */\n onSend: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n /**\n * Signal the local user is typing (R14). Called as the user edits the textarea; the\n * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.\n * Optional — absent on the firestore transport (no typing broadcast).\n */\n onTyping?: () => void;\n\n // Attachment support\n attachmentConfig?: ChatAttachmentConfig;\n /**\n * Attachment send. Composer uploads the file to Storage first, then calls\n * this with the resulting metadata. Consumer wires it to a backend callable\n * (`startUpload`) that writes the pendingMedia doc. The processor creates\n * the message doc after moderation succeeds.\n * Required for attachment functionality.\n */\n sendAttachment?: SendAttachmentFn;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n mentionConfig?: import('../types.js').ChatMentionConfig;\n};\n\nexport function Composer(props: ComposerProps) {\n const {\n onSend,\n onTyping,\n attachmentConfig,\n sendAttachment,\n disabled,\n autoFocus = false,\n placeholder = \"Type a message...\",\n } = props;\n\n const [text, setText] = React.useState(\"\");\n const [pendingFile, setPendingFile] = React.useState<File | null>(null);\n const [isSending, setIsSending] = React.useState(false);\n const [uploadState, setUploadState] = React.useState<UploadState | null>(null);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n const abortControllerRef = React.useRef<AbortController | null>(null);\n const guardedUpload = useGuardedUpload();\n // Navigation guard for the in-flight send: a send that has left the composer\n // but not yet committed is killable by navigation/sign-out with zero warning\n // (observed live: the callable aborted, the message silently never delivered).\n // Registering with the app's one guarded-navigation system makes beforeunload\n // fire on hard navs and lets GuardedLink-style navigation confirm first.\n // Optional accessor — consumers without the provider degrade to old behavior.\n const navigationGuard = useOptionalLocalUploadGuard();\n\n const mentionApi = useMentionAutocomplete({\n textareaRef: ref as React.RefObject<HTMLTextAreaElement>,\n value: text,\n onChange: setText,\n providers: props.mentionConfig?.providers ?? [],\n context: props.mentionConfig?.context,\n recent: props.mentionConfig?.recent,\n trigger: props.mentionConfig?.trigger,\n minQueryLength: props.mentionConfig?.minQueryLength,\n searchDebounceMs: props.mentionConfig?.searchDebounceMs,\n });\n\n const attachEnabled = Boolean(attachmentConfig && sendAttachment);\n\n // focus stability: never steal focus unless explicitly enabled\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n const handleFileSelected = React.useCallback((payload: MediaInputChangePayload) => {\n if (payload.error || !payload.file) {\n setPendingFile(null);\n return;\n }\n setPendingFile(payload.file);\n }, []);\n\n const handleCancelUpload = React.useCallback(() => {\n abortControllerRef.current?.abort();\n }, []);\n\n const send = async () => {\n const v = text.trim();\n if (!v && !pendingFile) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n if (pendingFile && attachmentConfig && sendAttachment) {\n // Attachment path: upload first via canonical guarded upload, then register.\n const uuid = genId();\n const { uploadAdapter, userId, storage } = attachmentConfig;\n const storagePath = uploadAdapter.buildUploadPath({ userId, attachmentId: uuid });\n const extraMetadata = uploadAdapter.buildUploadMetadata?.({ userId, attachmentId: uuid }) ?? {};\n\n const fileToUpload = ensureFileWithContentType(pendingFile);\n const attType = getAttachmentType(fileToUpload);\n\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n await guardedUpload({\n storage,\n path: storagePath,\n file: fileToUpload,\n metadata: { contentType: fileToUpload.type, ...extraMetadata },\n uploadId: uuid,\n onProgress: (state) => setUploadState(state),\n signal: controller.signal,\n });\n\n await sendAttachment({\n text: mentionApi.getValueWithTokens(),\n attachment: {\n attachmentId: uuid,\n storagePath,\n originalFileName: fileToUpload.name,\n type: attType,\n size: fileToUpload.size,\n },\n });\n } else {\n // Text-only path.\n await onSend(mentionApi.getValueWithTokens());\n }\n\n setText(\"\");\n setPendingFile(null);\n mentionApi.close();\n } catch (err) {\n // AbortError = user-initiated cancel via the MediaInput cancel button.\n // Clear local state and swallow — not a real failure.\n if (err instanceof Error && err.name === 'AbortError') {\n setPendingFile(null);\n return;\n }\n // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the\n // user's text — `setText(\"\")` only runs on the success path above — and must NOT\n // re-throw into the click handler (an unhandled rejection). Surface it so the\n // user can retry with their text intact.\n console.error(\"[Composer] Send failed:\", err);\n setPendingFile(null);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n abortControllerRef.current = null;\n setIsSending(false);\n setUploadState(null);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {pendingFile && !isSending && (\n <div className=\"chat-composer-file-preview\">\n <AttachmentTypeIcon type={getAttachmentType(pendingFile)} />\n <span className=\"chat-composer-file-name\">\n {getSelectedAttachmentLabel(getAttachmentType(pendingFile))}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-6 w-6 shrink-0\"\n onClick={() => setPendingFile(null)}\n >\n <X className=\"h-3 w-3\" />\n </Button>\n </div>\n )}\n\n {props.mentionConfig && mentionApi.state.open && (\n <div className=\"relative\">\n <div className=\"absolute bottom-full left-0 mb-1 z-10\">\n <MentionAutocomplete\n state={mentionApi.state}\n onSelect={(ref) => mentionApi.insertMention(ref)}\n />\n </div>\n </div>\n )}\n\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n // R14: broadcast a typing signal (transport-coalesced) while there is content.\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn(\"min-h-[40px] resize-none\")}\n onKeyDown={(e) => {\n if (mentionApi.handleKeyDown(e)) return;\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || (!text.trim() && !pendingFile)}\n onClick={send}\n >\n {isSending ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : \"Send\"}\n </Button>\n </div>\n\n {attachEnabled && attachmentConfig && (\n <div className=\"mt-2\">\n <MediaInput\n spec={attachmentConfig.attachmentSpec}\n onChange={handleFileSelected}\n selectedFile={pendingFile}\n isLoading={isSending}\n uploadState={uploadState}\n disabled={isDisabled}\n buttonLabel=\"Attach file\"\n onClear={() => setPendingFile(null)}\n onCancel={handleCancelUpload}\n className=\"w-full\"\n />\n </div>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"Composer.js","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button, Textarea } from \"@ttt-productions/ui-core/react\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { ensureFileWithContentType } from \"@ttt-productions/file-input\";\nimport { MediaInput } from \"@ttt-productions/file-input/react\";\nimport type { MediaInputChangePayload, MediaInputHandle } from \"@ttt-productions/file-input\";\nimport type { UploadState } from \"@ttt-productions/media-schemas\";\nimport { useGuardedUpload } from \"@ttt-productions/upload-ui/react/upload\";\nimport { useOptionalLocalUploadGuard } from \"@ttt-productions/upload-ui/react/guard\";\nimport { Loader2, X, FileText, ImageIcon, VideoIcon, MicIcon } from \"lucide-react\";\nimport type { ChatAttachment, ChatMessageV1, SendAttachmentFn } from \"@ttt-productions/chat-core\";\nimport type { ChatAttachmentConfig } from \"../types.js\";\nimport { useMentionAutocomplete } from \"../mentions/use-mention-autocomplete.js\";\nimport { MentionAutocomplete } from \"../mentions/MentionAutocomplete.js\";\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nfunction getAttachmentType(file: File): ChatAttachment[\"type\"] {\n if (file.type.startsWith(\"image/\")) return \"image\";\n if (file.type.startsWith(\"video/\")) return \"video\";\n if (file.type.startsWith(\"audio/\")) return \"audio\";\n return \"text\";\n}\n\nfunction AttachmentTypeIcon({ type }: { type: ChatAttachment[\"type\"] }) {\n switch (type) {\n case \"image\": return <ImageIcon className=\"h-4 w-4 shrink-0\" />;\n case \"video\": return <VideoIcon className=\"h-4 w-4 shrink-0\" />;\n case \"audio\": return <MicIcon className=\"h-4 w-4 shrink-0\" />;\n default: return <FileText className=\"h-4 w-4 shrink-0\" />;\n }\n}\n\nfunction getSelectedAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image selected\";\n case \"video\": return \"Video selected\";\n case \"audio\": return \"Audio selected\";\n default: return \"File selected\";\n }\n}\n\nexport type ComposerProps = {\n /**\n * Text-only send. Composer calls this when there is no pending attachment.\n * Consumer creates the message doc. Return value is ignored.\n */\n onSend: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n /**\n * Signal the local user is typing (R14). Called as the user edits the textarea; the\n * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.\n * Optional — absent on the firestore transport (no typing broadcast).\n */\n onTyping?: () => void;\n\n // Attachment support\n attachmentConfig?: ChatAttachmentConfig;\n /**\n * Attachment send. Composer uploads the file to Storage first, then calls\n * this with the resulting metadata. Consumer wires it to a backend callable\n * (`startUpload`) that writes the pendingMedia doc. The processor creates\n * the message doc after moderation succeeds.\n * Required for attachment functionality.\n */\n sendAttachment?: SendAttachmentFn;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n mentionConfig?: import('../types.js').ChatMentionConfig;\n};\n\n/**\n * Imperative handle exposed by {@link Composer} via `ref`. Additive — ref-less\n * usage is unaffected. ChatShell holds this ref so a failed-attachment bubble's\n * \"Attach again\" action can re-open the canonical picker.\n */\nexport type ComposerHandle = {\n /**\n * Open the attachment picker, delegating to the underlying MediaInput's\n * `openSelection()`. Must be called synchronously from a user gesture. No-op\n * when attachments are not configured (the MediaInput is not mounted) or the\n * composer is disabled/sending (MediaInput's own gate no-ops it).\n */\n openAttachmentSelector: () => void;\n};\n\nexport const Composer = React.forwardRef<ComposerHandle, ComposerProps>(function Composer(props, forwardedRef) {\n const {\n onSend,\n onTyping,\n attachmentConfig,\n sendAttachment,\n disabled,\n autoFocus = false,\n placeholder = \"Type a message...\",\n } = props;\n\n const [text, setText] = React.useState(\"\");\n const [pendingFile, setPendingFile] = React.useState<File | null>(null);\n const [isSending, setIsSending] = React.useState(false);\n const [uploadState, setUploadState] = React.useState<UploadState | null>(null);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n const abortControllerRef = React.useRef<AbortController | null>(null);\n const mediaInputRef = React.useRef<MediaInputHandle>(null);\n const guardedUpload = useGuardedUpload();\n // Navigation guard for the in-flight send: a send that has left the composer\n // but not yet committed is killable by navigation/sign-out with zero warning\n // (observed live: the callable aborted, the message silently never delivered).\n // Registering with the app's one guarded-navigation system makes beforeunload\n // fire on hard navs and lets GuardedLink-style navigation confirm first.\n // Optional accessor — consumers without the provider degrade to old behavior.\n const navigationGuard = useOptionalLocalUploadGuard();\n\n const mentionApi = useMentionAutocomplete({\n textareaRef: ref as React.RefObject<HTMLTextAreaElement>,\n value: text,\n onChange: setText,\n providers: props.mentionConfig?.providers ?? [],\n context: props.mentionConfig?.context,\n recent: props.mentionConfig?.recent,\n trigger: props.mentionConfig?.trigger,\n minQueryLength: props.mentionConfig?.minQueryLength,\n searchDebounceMs: props.mentionConfig?.searchDebounceMs,\n });\n\n const attachEnabled = Boolean(attachmentConfig && sendAttachment);\n\n // focus stability: never steal focus unless explicitly enabled\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n const handleFileSelected = React.useCallback((payload: MediaInputChangePayload) => {\n if (payload.error || !payload.file) {\n setPendingFile(null);\n return;\n }\n setPendingFile(payload.file);\n }, []);\n\n const handleCancelUpload = React.useCallback(() => {\n abortControllerRef.current?.abort();\n }, []);\n\n // Additive imperative handle: re-open the canonical attachment picker. Delegates\n // straight to MediaInput's openSelection so onBeforeSelect / disabled / validation\n // / crop are all honored (and the disabled/sending gate is MediaInput's — it\n // receives `disabled`/`isLoading` below). No-op when attachments aren't configured\n // (MediaInput is unmounted → ref is null).\n const openAttachmentSelector = React.useCallback(() => {\n mediaInputRef.current?.openSelection();\n }, []);\n React.useImperativeHandle(forwardedRef, () => ({ openAttachmentSelector }), [openAttachmentSelector]);\n\n const send = async () => {\n const v = text.trim();\n if (!v && !pendingFile) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n if (pendingFile && attachmentConfig && sendAttachment) {\n // Attachment path: upload first via canonical guarded upload, then register.\n const uuid = genId();\n const { uploadAdapter, userId, storage } = attachmentConfig;\n const storagePath = uploadAdapter.buildUploadPath({ userId, attachmentId: uuid });\n const extraMetadata = uploadAdapter.buildUploadMetadata?.({ userId, attachmentId: uuid }) ?? {};\n\n const fileToUpload = ensureFileWithContentType(pendingFile);\n const attType = getAttachmentType(fileToUpload);\n\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n await guardedUpload({\n storage,\n path: storagePath,\n file: fileToUpload,\n metadata: { contentType: fileToUpload.type, ...extraMetadata },\n uploadId: uuid,\n onProgress: (state) => setUploadState(state),\n signal: controller.signal,\n });\n\n await sendAttachment({\n text: mentionApi.getValueWithTokens(),\n attachment: {\n attachmentId: uuid,\n storagePath,\n originalFileName: fileToUpload.name,\n type: attType,\n size: fileToUpload.size,\n },\n });\n } else {\n // Text-only path.\n await onSend(mentionApi.getValueWithTokens());\n }\n\n setText(\"\");\n setPendingFile(null);\n mentionApi.close();\n } catch (err) {\n // AbortError = user-initiated cancel via the MediaInput cancel button.\n // Clear local state and swallow — not a real failure.\n if (err instanceof Error && err.name === 'AbortError') {\n setPendingFile(null);\n return;\n }\n // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the\n // user's text — `setText(\"\")` only runs on the success path above — and must NOT\n // re-throw into the click handler (an unhandled rejection). Surface it so the\n // user can retry with their text intact.\n console.error(\"[Composer] Send failed:\", err);\n setPendingFile(null);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n abortControllerRef.current = null;\n setIsSending(false);\n setUploadState(null);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {pendingFile && !isSending && (\n <div className=\"chat-composer-file-preview\">\n <AttachmentTypeIcon type={getAttachmentType(pendingFile)} />\n <span className=\"chat-composer-file-name\">\n {getSelectedAttachmentLabel(getAttachmentType(pendingFile))}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-6 w-6 shrink-0\"\n onClick={() => setPendingFile(null)}\n >\n <X className=\"h-3 w-3\" />\n </Button>\n </div>\n )}\n\n {props.mentionConfig && mentionApi.state.open && (\n <div className=\"relative\">\n <div className=\"absolute bottom-full left-0 mb-1 z-10\">\n <MentionAutocomplete\n state={mentionApi.state}\n onSelect={(ref) => mentionApi.insertMention(ref)}\n />\n </div>\n </div>\n )}\n\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n // R14: broadcast a typing signal (transport-coalesced) while there is content.\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn(\"min-h-[40px] resize-none\")}\n onKeyDown={(e) => {\n if (mentionApi.handleKeyDown(e)) return;\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || (!text.trim() && !pendingFile)}\n onClick={send}\n >\n {isSending ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : \"Send\"}\n </Button>\n </div>\n\n {attachEnabled && attachmentConfig && (\n <div className=\"mt-2\">\n <MediaInput\n ref={mediaInputRef}\n spec={attachmentConfig.attachmentSpec}\n onChange={handleFileSelected}\n selectedFile={pendingFile}\n isLoading={isSending}\n uploadState={uploadState}\n disabled={isDisabled}\n buttonLabel=\"Attach file\"\n onClear={() => setPendingFile(null)}\n onCancel={handleCancelUpload}\n className=\"w-full\"\n />\n </div>\n )}\n </div>\n );\n});\n"]}
@@ -10,6 +10,11 @@ export type MessageItemDefaultProps = {
10
10
  * ChatShell on the realtime transport; absent on firestore). Enables the
11
11
  * retry affordance on a `meta.sendFailed` bubble. */
12
12
  onRetrySend?: (clientMessageId: string) => void;
13
+ /** Re-open the attachment picker for a terminally-failed attachment (wired by
14
+ * ChatShell to the Composer's `openAttachmentSelector`; present only when
15
+ * attachment support is configured). Enables the "Attach again" action on a
16
+ * failed attachment bubble, shown only to the sender. */
17
+ onRetryAttachment?: () => void;
13
18
  };
14
19
  export declare function MessageItemDefault(props: MessageItemDefaultProps): import("react").JSX.Element;
15
20
  //# sourceMappingURL=MessageItemDefault.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAiIpG,MAAM,MAAM,uBAAuB,GAAG;IACpC,CAAC,EAAE,aAAa,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAG9B,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhE;;0DAEsD;IACtD,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CAEjD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAsGhE"}
1
+ {"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAsMpG,MAAM,MAAM,uBAAuB,GAAG;IACpC,CAAC,EAAE,aAAa,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAG9B,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhE;;0DAEsD;IACtD,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhD;;;8DAG0D;IAC1D,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAEhC,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BA2GhE"}
@@ -20,7 +20,46 @@ function getAttachmentLabel(type) {
20
20
  default: return "File attachment";
21
21
  }
22
22
  }
23
- function AttachmentView({ att }) {
23
+ // Safe, generic terminal-failure copy per kind. The backend `failureReason` is an
24
+ // INTERNAL diagnostic string and must never reach the UI (it could leak processing
25
+ // internals); it is mapped to this fixed copy instead.
26
+ function getAttachmentFailureCopy(type) {
27
+ switch (type) {
28
+ case "image": return "This image could not be processed.";
29
+ case "video": return "This video could not be processed.";
30
+ case "audio": return "This audio could not be processed.";
31
+ default: return "This file could not be processed.";
32
+ }
33
+ }
34
+ // ============================================
35
+ // Correlated send-rejection copy + retry policy
36
+ // ============================================
37
+ // User-facing copy per correlated `send-rejected` code (stamped on meta.sendFailureCode
38
+ // by the realtime channel client). A failed send with no code (a transport/exhausted
39
+ // reconnect failure) falls back to the neutral "Couldn't send".
40
+ const SEND_FAILURE_COPY = {
41
+ "blocked-word": "Message contains blocked language",
42
+ "archived": "This channel is read-only",
43
+ "deleted": "This channel is read-only",
44
+ "membership-pending": "Chat is still preparing",
45
+ "wordlist-unavailable": "Chat safety check is temporarily unavailable",
46
+ "flood": "Please wait before sending again",
47
+ "slow-mode": "Please wait before sending again",
48
+ };
49
+ function sendFailureCopy(code) {
50
+ return (code && SEND_FAILURE_COPY[code]) || "Couldn't send";
51
+ }
52
+ // Retry policy comes from meta.sendRetryable, stamped by the channel client from
53
+ // the wire contract's canonical retryability table (chat-schemas enforces the
54
+ // frame's `retryable` matches the table, so this is the ONE source of truth —
55
+ // never a second hardcoded code list here, which would have to move in lockstep
56
+ // with the contract). `false` = terminal (an unchanged resend is guaranteed to
57
+ // fail — no Retry action); `true` or ABSENT (a code-less transport/exhausted
58
+ // reconnect failure) keeps Retry, reusing the ORIGINAL clientMessageId.
59
+ function isRetryableFailure(meta) {
60
+ return meta?.sendRetryable !== false;
61
+ }
62
+ function AttachmentView({ att, mine, onRetryAttachment, }) {
24
63
  // Display URL is built at render time by the app-injected resolver from
25
64
  // att.mediaAssetId — attachments never store URLs (protected-gateway model).
26
65
  const resolveAttachmentUrl = useChatAttachmentUrlResolver();
@@ -28,15 +67,19 @@ function AttachmentView({ att }) {
28
67
  // app's ONE display path (recovery adapter, diagnostics, telemetry). Falls
29
68
  // back to the package's own MediaViewer when no provider is present.
30
69
  const Media = useChatAttachmentMediaComponent() ?? MediaViewer;
31
- // In-flight placeholder — bytes uploaded, processing/moderation pending. No
32
- // `mediaAssetId` yet. Rendered only to the sender (other participants can't
33
- // read a pending doc — Firestore rules scope it until `status === 'ready'`).
70
+ // In-flight placeholder — bytes uploaded, backend processing/moderation pending.
71
+ // No `mediaAssetId` yet. Rendered only to the sender (other participants can't
72
+ // read a pending doc — Firestore rules scope it until `status === 'ready'`). The
73
+ // bytes are already uploaded, so the honest copy is "Processing…" (NOT "Sending…").
34
74
  if (att.status === "pending") {
35
- return (_jsxs("div", { className: "chat-attachment-pending", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "Sending\u2026" })] }));
75
+ return (_jsxs("div", { className: "chat-attachment-pending", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "Processing\u2026" })] }));
36
76
  }
37
- // Processing/moderation rejected the attachment — sender-only.
77
+ // Processing/moderation rejected the attachment — sender-only. The backend
78
+ // `failureReason` is internal and NEVER rendered; a fixed safe per-kind copy is
79
+ // shown instead. The sender can pick a fresh file through the canonical picker
80
+ // ("Attach again"), offered only when the composer wired a retry handler.
38
81
  if (att.status === "failed") {
39
- return (_jsxs("div", { className: "chat-attachment-rejected", children: [_jsx(AlertTriangle, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: att.failureReason ?? "Couldn't send this attachment" })] }));
82
+ return (_jsxs("div", { className: "chat-attachment-rejected", role: "alert", children: [_jsx(AlertTriangle, { className: "h-4 w-4 shrink-0 chat-attachment-rejected-icon" }), _jsxs("div", { className: "chat-attachment-rejected-body", children: [_jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-rejected-reason", children: getAttachmentFailureCopy(att.type) })] }), mine && onRetryAttachment && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "chat-attachment-retry", onClick: onRetryAttachment, children: "Attach again" }))] }));
40
83
  }
41
84
  // ready (or legacy/absent status) — resolve the display URL from the asset ref.
42
85
  const url = resolveAttachmentUrl(att);
@@ -49,7 +92,7 @@ function AttachmentView({ att }) {
49
92
  // "Sending…", NO filename) and do NOT mount the media/download renderer until a
50
93
  // non-empty authorized URL exists. Sender-only pending/processing/failed states
51
94
  // above are unchanged — this begins only after a terminal-ready row is received.
52
- return (_jsxs("div", { className: "chat-attachment-pending", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "Loading\u2026" })] }));
95
+ return (_jsxs("div", { className: "chat-attachment-pending", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "Loading\u2026" })] }));
53
96
  }
54
97
  if (att.type === "image") {
55
98
  return _jsx(Media, { type: "image", url: url, alt: "Image attachment", className: "chat-attachment-media" });
@@ -77,7 +120,7 @@ function SystemMessage({ m }) {
77
120
  return (_jsx("div", { className: "chat-system-message", children: m.text && _jsx("span", { children: m.text }) }));
78
121
  }
79
122
  export function MessageItemDefault(props) {
80
- const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend } = props;
123
+ const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend, onRetryAttachment } = props;
81
124
  const senderName = useResolvedSenderName(m.senderId);
82
125
  // System messages render differently
83
126
  if (m.isSystemMessage) {
@@ -91,9 +134,13 @@ export function MessageItemDefault(props) {
91
134
  const sendFailed = m.meta?.sendFailed === true;
92
135
  const sendPending = m.meta?.optimistic === true && !sendFailed;
93
136
  const clientMessageId = typeof m.meta?.clientMessageId === "string" ? m.meta.clientMessageId : null;
137
+ // Correlated rejection reason (from the `send-rejected` frame), if any — drives
138
+ // both the failure copy and whether a Retry affordance is offered.
139
+ const sendFailureCode = typeof m.meta?.sendFailureCode === "string" ? m.meta.sendFailureCode : null;
140
+ const showRetry = sendFailed && isRetryableFailure(m.meta);
94
141
  return (_jsxs("div", { className: cn("flex flex-col w-fit max-w-[85%]", mine ? "ml-auto items-end" : "mr-auto items-start", isContinuation ? "chat-continuation-gap" : "chat-group-gap"), children: [_jsxs("div", { className: cn("chat-bubble", mine ? "chat-bubble--mine" : "chat-bubble--theirs", sendPending && "chat-bubble--pending", sendFailed && "chat-bubble--failed"), children: [!isContinuation && (_jsxs("div", { className: "flex items-center gap-2 text-xs opacity-80 mb-1", children: [_jsx("span", { className: cn("font-medium", onSenderClick && "cursor-pointer hover:underline"), onClick: onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined, role: onSenderClick ? "button" : undefined, tabIndex: onSenderClick ? 0 : undefined, onKeyDown: onSenderClick ? (e) => {
95
142
  if (e.key === "Enter" || e.key === " ")
96
143
  onSenderClick(m.senderId, senderName);
97
- } : undefined, children: senderName }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })] })), m.replyTo && _jsx(ReplyQuote, { replyTo: m.replyTo }), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) })), m.attachment && (_jsx("div", { className: "mt-2", children: _jsx(AttachmentView, { att: m.attachment }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }), _jsx("span", { children: "Sending\u2026" })] })), sendFailed && (_jsxs("div", { className: "chat-send-status chat-send-status--failed mt-0.5", role: "alert", children: [_jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }), _jsx("span", { children: "Couldn't send" }), onRetrySend && clientMessageId && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-5 px-1.5 text-[0.6875rem]", onClick: () => onRetrySend(clientMessageId), children: "Retry" }))] })), !isContinuation && (_jsx("div", { className: "mt-1", children: _jsx(MessageActions, { messageId: m.messageId, isAdmin: isAdmin, handlers: handlers }) }))] }));
144
+ } : undefined, children: senderName }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })] })), m.replyTo && _jsx(ReplyQuote, { replyTo: m.replyTo }), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) })), m.attachment && (_jsx("div", { className: "mt-2", children: _jsx(AttachmentView, { att: m.attachment, mine: mine, onRetryAttachment: onRetryAttachment }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }), _jsx("span", { children: "Sending\u2026" })] })), sendFailed && (_jsxs("div", { className: "chat-send-status chat-send-status--failed mt-0.5", role: "alert", children: [_jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }), _jsx("span", { children: sendFailureCopy(sendFailureCode) }), showRetry && onRetrySend && clientMessageId && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-5 px-1.5 text-[0.6875rem]", onClick: () => onRetrySend(clientMessageId), children: "Retry" }))] })), !isContinuation && (_jsx("div", { className: "mt-1", children: _jsx(MessageActions, { messageId: m.messageId, isAdmin: isAdmin, handlers: handlers }) }))] }));
98
145
  }
99
146
  //# sourceMappingURL=MessageItemDefault.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MessageItemDefault.js","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAGb,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,EAAE,+BAA+B,EAAE,MAAM,0CAA0C,CAAC;AAE3F,+CAA+C;AAC/C,uBAAuB;AACvB,+CAA+C;AAE/C,SAAS,kBAAkB,CAAC,IAA4B;IACtD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,OAAO,CAAC,CAAC,OAAO,kBAAkB,CAAC;QACxC,KAAK,OAAO,CAAC,CAAC,OAAO,kBAAkB,CAAC;QACxC,KAAK,OAAO,CAAC,CAAC,OAAO,kBAAkB,CAAC;QACxC,OAAO,CAAC,CAAC,OAAO,iBAAiB,CAAC;IACpC,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,EAAE,GAAG,EAA2B;IACtD,wEAAwE;IACxE,6EAA6E;IAC7E,MAAM,oBAAoB,GAAG,4BAA4B,EAAE,CAAC;IAC5D,2EAA2E;IAC3E,2EAA2E;IAC3E,qEAAqE;IACrE,MAAM,KAAK,GAAG,+BAA+B,EAAE,IAAI,WAAW,CAAC;IAC/D,4EAA4E;IAC5E,4EAA4E;IAC5E,6EAA6E;IAC7E,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,CACL,eAAK,SAAS,EAAC,yBAAyB,aACtC,KAAC,OAAO,IAAC,SAAS,EAAC,+BAA+B,GAAG,EACrD,eAAM,SAAS,EAAC,UAAU,YAAE,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,GAAQ,EAChE,eAAM,SAAS,EAAC,8BAA8B,8BAAgB,IAC1D,CACP,CAAC;IACJ,CAAC;IACD,+DAA+D;IAC/D,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,CACL,eAAK,SAAS,EAAC,0BAA0B,aACvC,KAAC,aAAa,IAAC,SAAS,EAAC,kBAAkB,GAAG,EAC9C,eAAM,SAAS,EAAC,UAAU,YAAE,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,GAAQ,EAChE,eAAM,SAAS,EAAC,8BAA8B,YAC3C,GAAG,CAAC,aAAa,IAAI,+BAA+B,GAChD,IACH,CACP,CAAC;IACJ,CAAC;IACD,gFAAgF;IAChF,MAAM,GAAG,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,8EAA8E;QAC9E,8EAA8E;QAC9E,+EAA+E;QAC/E,2EAA2E;QAC3E,yEAAyE;QACzE,gFAAgF;QAChF,gFAAgF;QAChF,iFAAiF;QACjF,OAAO,CACL,eAAK,SAAS,EAAC,yBAAyB,aACtC,KAAC,OAAO,IAAC,SAAS,EAAC,+BAA+B,GAAG,EACrD,eAAM,SAAS,EAAC,UAAU,YAAE,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,GAAQ,EAChE,eAAM,SAAS,EAAC,8BAA8B,8BAAgB,IAC1D,CACP,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,KAAC,KAAK,IAAC,IAAI,EAAC,OAAO,EAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAC,kBAAkB,EAAC,SAAS,EAAC,uBAAuB,GAAG,CAAC;IACnG,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,KAAC,KAAK,IAAC,IAAI,EAAC,OAAO,EAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,QAAC,SAAS,EAAC,uBAAuB,GAAG,CAAC;IACrF,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,KAAC,KAAK,IAAC,IAAI,EAAC,OAAO,EAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,QAAC,SAAS,EAAC,uBAAuB,GAAG,CAAC;IACrF,CAAC;IACD,gCAAgC;IAChC,OAAO,CACL,aACE,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,GAAG,CAAC,IAAI,EAClB,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,qBAAqB,EACzB,SAAS,EAAC,2BAA2B,aAErC,KAAC,QAAQ,IAAC,SAAS,EAAC,kBAAkB,GAAG,EACzC,eAAM,SAAS,EAAC,UAAU,oCAA2B,IACnD,CACL,CAAC;AACJ,CAAC;AAED,+CAA+C;AAC/C,iBAAiB;AACjB,+CAA+C;AAE/C,SAAS,UAAU,CAAC,EAAE,OAAO,EAAsD;IACjF,MAAM,SAAS,GAAG,qBAAqB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,OAAO,CACL,eAAK,SAAS,EAAC,kBAAkB,aAC/B,eAAM,SAAS,EAAC,yBAAyB,YAAE,SAAS,GAAQ,EAC5D,eAAM,SAAS,EAAC,0BAA0B,YAAC,KAAC,WAAW,IAAC,IAAI,EAAE,OAAO,CAAC,cAAc,GAAI,GAAO,IAC3F,CACP,CAAC;AACJ,CAAC;AAED,+CAA+C;AAC/C,iBAAiB;AACjB,+CAA+C;AAE/C,SAAS,aAAa,CAAC,EAAE,CAAC,EAAwB;IAChD,OAAO,CACL,cAAK,SAAS,EAAC,qBAAqB,YACjC,CAAC,CAAC,IAAI,IAAI,yBAAO,CAAC,CAAC,IAAI,GAAQ,GAC5B,CACP,CAAC;AACJ,CAAC;AAyBD,MAAM,UAAU,kBAAkB,CAAC,KAA8B;IAC/D,MAAM,EAAE,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IAClG,MAAM,UAAU,GAAG,qBAAqB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAErD,qCAAqC;IACrC,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;QACtB,OAAO,KAAC,aAAa,IAAC,CAAC,EAAE,CAAC,GAAI,CAAC;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,KAAK,aAAa,CAAC;IAE1C,8EAA8E;IAC9E,gFAAgF;IAChF,+EAA+E;IAC/E,6CAA6C;IAC7C,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IAC/C,MAAM,WAAW,GAAG,CAAC,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC;IAC/D,MAAM,eAAe,GACnB,OAAO,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC;IAE9E,OAAO,CACL,eACE,SAAS,EAAE,EAAE,CACX,iCAAiC,EACjC,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB,EAClD,cAAc,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,gBAAgB,CAC5D,aAED,eACE,SAAS,EAAE,EAAE,CACX,aAAa,EACb,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB,EAClD,WAAW,IAAI,sBAAsB,EACrC,UAAU,IAAI,qBAAqB,CACpC,aAEA,CAAC,cAAc,IAAI,CAClB,eAAK,SAAS,EAAC,iDAAiD,aAC9D,eACE,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,aAAa,IAAI,gCAAgC,CAAC,EAC/E,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,EAChF,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAC1C,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,EACvC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;oCAC/B,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;wCAAE,aAAa,CAAC,CAAC,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;gCAChF,CAAC,CAAC,CAAC,CAAC,SAAS,YAEZ,UAAU,GACN,EACP,oCAAc,EACd,yBAAO,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,GAAQ,IAC/F,CACP,EAEA,CAAC,CAAC,OAAO,IAAI,KAAC,UAAU,IAAC,OAAO,EAAE,CAAC,CAAC,OAAO,GAAI,EAE/C,CAAC,CAAC,IAAI,IAAI,CACT,YAAG,SAAS,EAAC,6BAA6B,YACxC,KAAC,WAAW,IAAC,IAAI,EAAE,CAAC,CAAC,IAAI,GAAI,GAC3B,CACL,EAEA,CAAC,CAAC,UAAU,IAAI,CACf,cAAK,SAAS,EAAC,MAAM,YACnB,KAAC,cAAc,IAAC,GAAG,EAAE,CAAC,CAAC,UAAU,GAAI,GACjC,CACP,IACG,EAIL,WAAW,IAAI,CACd,eAAK,SAAS,EAAC,yBAAyB,EAAC,IAAI,EAAC,QAAQ,eAAW,QAAQ,aACvE,KAAC,OAAO,IAAC,SAAS,EAAC,+BAA+B,GAAG,EACrD,2CAAqB,IACjB,CACP,EACA,UAAU,IAAI,CACb,eAAK,SAAS,EAAC,kDAAkD,EAAC,IAAI,EAAC,OAAO,aAC5E,KAAC,aAAa,IAAC,SAAS,EAAC,kBAAkB,GAAG,EAC9C,2CAA+B,EAC9B,WAAW,IAAI,eAAe,IAAI,CACjC,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,6BAA6B,EACvC,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,sBAGpC,CACV,IACG,CACP,EAEA,CAAC,cAAc,IAAI,CAClB,cAAK,SAAS,EAAC,MAAM,YACnB,KAAC,cAAc,IAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAI,GAC5E,CACP,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport type { ChatMessageV1, ChatAttachment, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport { MessageText } from \"../mentions/MessageText.js\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Button } from \"@ttt-productions/ui-core/react\";\nimport { MediaViewer } from \"@ttt-productions/media-viewer/react\";\nimport { FileText, Loader2, AlertTriangle } from \"lucide-react\";\nimport { MessageActions } from \"./menus.js\";\nimport { useResolvedSenderName } from \"../context/ChatNameResolverContext.js\";\nimport { useChatAttachmentUrlResolver } from \"../context/ChatAttachmentUrlContext.js\";\nimport { useChatAttachmentMediaComponent } from \"../context/ChatAttachmentMediaContext.js\";\n\n// ============================================\n// Attachment rendering\n// ============================================\n\nfunction getAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image attachment\";\n case \"video\": return \"Video attachment\";\n case \"audio\": return \"Audio attachment\";\n default: return \"File attachment\";\n }\n}\n\nfunction AttachmentView({ att }: { att: ChatAttachment }) {\n // Display URL is built at render time by the app-injected resolver from\n // att.mediaAssetId — attachments never store URLs (protected-gateway model).\n const resolveAttachmentUrl = useChatAttachmentUrlResolver();\n // App-injected media component (optional) — lets chat attachments ride the\n // app's ONE display path (recovery adapter, diagnostics, telemetry). Falls\n // back to the package's own MediaViewer when no provider is present.\n const Media = useChatAttachmentMediaComponent() ?? MediaViewer;\n // In-flight placeholder — bytes uploaded, processing/moderation pending. No\n // `mediaAssetId` yet. Rendered only to the sender (other participants can't\n // read a pending doc — Firestore rules scope it until `status === 'ready'`).\n if (att.status === \"pending\") {\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Sending…</span>\n </div>\n );\n }\n // Processing/moderation rejected the attachment — sender-only.\n if (att.status === \"failed\") {\n return (\n <div className=\"chat-attachment-rejected\">\n <AlertTriangle className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">\n {att.failureReason ?? \"Couldn't send this attachment\"}\n </span>\n </div>\n );\n }\n // ready (or legacy/absent status) — resolve the display URL from the asset ref.\n const url = resolveAttachmentUrl(att);\n if (!url) {\n // Terminal-ready row, but the authorized URL is still SETTLING (the grant/URL\n // resolver has nothing yet). Returning null left only the surrounding message\n // metadata → an observed empty bubble; a broken/grant-less <img> would flash a\n // 403. Render a neutral loading placeholder in the existing loading visual\n // language (spinner + kind-appropriate generic label + \"Loading…\", never\n // \"Sending…\", NO filename) and do NOT mount the media/download renderer until a\n // non-empty authorized URL exists. Sender-only pending/processing/failed states\n // above are unchanged — this begins only after a terminal-ready row is received.\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Loading…</span>\n </div>\n );\n }\n if (att.type === \"image\") {\n return <Media type=\"image\" url={url} alt=\"Image attachment\" className=\"chat-attachment-media\" />;\n }\n if (att.type === \"video\") {\n return <Media type=\"video\" url={url} controls className=\"chat-attachment-media\" />;\n }\n if (att.type === \"audio\") {\n return <Media type=\"audio\" url={url} controls className=\"chat-attachment-media\" />;\n }\n // text/markdown — download link\n return (\n <a\n href={url}\n download={att.name}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"chat-attachment-text-link\"\n >\n <FileText className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">Download attachment</span>\n </a>\n );\n}\n\n// ============================================\n// Reply-to quote\n// ============================================\n\nfunction ReplyQuote({ replyTo }: { replyTo: NonNullable<ChatMessageV1[\"replyTo\"]> }) {\n const replyName = useResolvedSenderName(replyTo.senderId);\n return (\n <div className=\"chat-reply-quote\">\n <span className=\"chat-reply-quote-sender\">{replyName}</span>\n <span className=\"chat-reply-quote-preview\"><MessageText text={replyTo.messagePreview} /></span>\n </div>\n );\n}\n\n// ============================================\n// System message\n// ============================================\n\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n </div>\n );\n}\n\n// ============================================\n// Default message item\n// ============================================\n\nexport type MessageItemDefaultProps = {\n m: ChatMessageV1;\n currentUserId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n\n // Grouping\n isContinuation?: boolean;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n /** Retry a failed realtime send by its original clientMessageId (wired by\n * ChatShell on the realtime transport; absent on firestore). Enables the\n * retry affordance on a `meta.sendFailed` bubble. */\n onRetrySend?: (clientMessageId: string) => void;\n\n};\n\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend } = props;\n const senderName = useResolvedSenderName(m.senderId);\n\n // System messages render differently\n if (m.isSystemMessage) {\n return <SystemMessage m={m} />;\n }\n\n const mine = m.senderId === currentUserId;\n\n // Realtime optimistic-send lifecycle (meta is stamped by the channel client):\n // an un-acked echo renders subtly pending; one past the retry/age cap renders a\n // visibly failed bubble with a retry affordance (same clientMessageId — the DO\n // dedups, so a retry can never double-send).\n const sendFailed = m.meta?.sendFailed === true;\n const sendPending = m.meta?.optimistic === true && !sendFailed;\n const clientMessageId =\n typeof m.meta?.clientMessageId === \"string\" ? m.meta.clientMessageId : null;\n\n return (\n <div\n className={cn(\n \"flex flex-col w-fit max-w-[85%]\",\n mine ? \"ml-auto items-end\" : \"mr-auto items-start\",\n isContinuation ? \"chat-continuation-gap\" : \"chat-group-gap\"\n )}\n >\n <div\n className={cn(\n \"chat-bubble\",\n mine ? \"chat-bubble--mine\" : \"chat-bubble--theirs\",\n sendPending && \"chat-bubble--pending\",\n sendFailed && \"chat-bubble--failed\"\n )}\n >\n {!isContinuation && (\n <div className=\"flex items-center gap-2 text-xs opacity-80 mb-1\">\n <span\n className={cn(\"font-medium\", onSenderClick && \"cursor-pointer hover:underline\")}\n onClick={onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined}\n role={onSenderClick ? \"button\" : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={onSenderClick ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onSenderClick(m.senderId, senderName);\n } : undefined}\n >\n {senderName}\n </span>\n <span>·</span>\n <span>{new Date(m.createdAt).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}</span>\n </div>\n )}\n\n {m.replyTo && <ReplyQuote replyTo={m.replyTo} />}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\n )}\n\n {m.attachment && (\n <div className=\"mt-2\">\n <AttachmentView att={m.attachment} />\n </div>\n )}\n </div>\n\n {/* Send-state status rows render regardless of grouping — a pending/failed\n send must never be indistinguishable from a delivered one. */}\n {sendPending && (\n <div className=\"chat-send-status mt-0.5\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-3 w-3 shrink-0 animate-spin\" />\n <span>Sending…</span>\n </div>\n )}\n {sendFailed && (\n <div className=\"chat-send-status chat-send-status--failed mt-0.5\" role=\"alert\">\n <AlertTriangle className=\"h-3 w-3 shrink-0\" />\n <span>Couldn&apos;t send</span>\n {onRetrySend && clientMessageId && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-5 px-1.5 text-[0.6875rem]\"\n onClick={() => onRetrySend(clientMessageId)}\n >\n Retry\n </Button>\n )}\n </div>\n )}\n\n {!isContinuation && (\n <div className=\"mt-1\">\n <MessageActions messageId={m.messageId} isAdmin={isAdmin} handlers={handlers} />\n </div>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"MessageItemDefault.js","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport type { ChatMessageV1, ChatAttachment, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport { MessageText } from \"../mentions/MessageText.js\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Button } from \"@ttt-productions/ui-core/react\";\nimport { MediaViewer } from \"@ttt-productions/media-viewer/react\";\nimport { FileText, Loader2, AlertTriangle } from \"lucide-react\";\nimport { MessageActions } from \"./menus.js\";\nimport { useResolvedSenderName } from \"../context/ChatNameResolverContext.js\";\nimport { useChatAttachmentUrlResolver } from \"../context/ChatAttachmentUrlContext.js\";\nimport { useChatAttachmentMediaComponent } from \"../context/ChatAttachmentMediaContext.js\";\n\n// ============================================\n// Attachment rendering\n// ============================================\n\nfunction getAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image attachment\";\n case \"video\": return \"Video attachment\";\n case \"audio\": return \"Audio attachment\";\n default: return \"File attachment\";\n }\n}\n\n// Safe, generic terminal-failure copy per kind. The backend `failureReason` is an\n// INTERNAL diagnostic string and must never reach the UI (it could leak processing\n// internals); it is mapped to this fixed copy instead.\nfunction getAttachmentFailureCopy(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"This image could not be processed.\";\n case \"video\": return \"This video could not be processed.\";\n case \"audio\": return \"This audio could not be processed.\";\n default: return \"This file could not be processed.\";\n }\n}\n\n// ============================================\n// Correlated send-rejection copy + retry policy\n// ============================================\n\n// User-facing copy per correlated `send-rejected` code (stamped on meta.sendFailureCode\n// by the realtime channel client). A failed send with no code (a transport/exhausted\n// reconnect failure) falls back to the neutral \"Couldn't send\".\nconst SEND_FAILURE_COPY: Record<string, string> = {\n \"blocked-word\": \"Message contains blocked language\",\n \"archived\": \"This channel is read-only\",\n \"deleted\": \"This channel is read-only\",\n \"membership-pending\": \"Chat is still preparing\",\n \"wordlist-unavailable\": \"Chat safety check is temporarily unavailable\",\n \"flood\": \"Please wait before sending again\",\n \"slow-mode\": \"Please wait before sending again\",\n};\n\nfunction sendFailureCopy(code: string | null): string {\n return (code && SEND_FAILURE_COPY[code]) || \"Couldn't send\";\n}\n\n// Retry policy comes from meta.sendRetryable, stamped by the channel client from\n// the wire contract's canonical retryability table (chat-schemas enforces the\n// frame's `retryable` matches the table, so this is the ONE source of truth —\n// never a second hardcoded code list here, which would have to move in lockstep\n// with the contract). `false` = terminal (an unchanged resend is guaranteed to\n// fail — no Retry action); `true` or ABSENT (a code-less transport/exhausted\n// reconnect failure) keeps Retry, reusing the ORIGINAL clientMessageId.\nfunction isRetryableFailure(meta: Record<string, unknown> | undefined): boolean {\n return meta?.sendRetryable !== false;\n}\n\nfunction AttachmentView({\n att,\n mine,\n onRetryAttachment,\n}: {\n att: ChatAttachment;\n mine: boolean;\n onRetryAttachment?: () => void;\n}) {\n // Display URL is built at render time by the app-injected resolver from\n // att.mediaAssetId — attachments never store URLs (protected-gateway model).\n const resolveAttachmentUrl = useChatAttachmentUrlResolver();\n // App-injected media component (optional) — lets chat attachments ride the\n // app's ONE display path (recovery adapter, diagnostics, telemetry). Falls\n // back to the package's own MediaViewer when no provider is present.\n const Media = useChatAttachmentMediaComponent() ?? MediaViewer;\n // In-flight placeholder — bytes uploaded, backend processing/moderation pending.\n // No `mediaAssetId` yet. Rendered only to the sender (other participants can't\n // read a pending doc — Firestore rules scope it until `status === 'ready'`). The\n // bytes are already uploaded, so the honest copy is \"Processing…\" (NOT \"Sending…\").\n if (att.status === \"pending\") {\n return (\n <div className=\"chat-attachment-pending\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Processing…</span>\n </div>\n );\n }\n // Processing/moderation rejected the attachment — sender-only. The backend\n // `failureReason` is internal and NEVER rendered; a fixed safe per-kind copy is\n // shown instead. The sender can pick a fresh file through the canonical picker\n // (\"Attach again\"), offered only when the composer wired a retry handler.\n if (att.status === \"failed\") {\n return (\n <div className=\"chat-attachment-rejected\" role=\"alert\">\n <AlertTriangle className=\"h-4 w-4 shrink-0 chat-attachment-rejected-icon\" />\n <div className=\"chat-attachment-rejected-body\">\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-rejected-reason\">\n {getAttachmentFailureCopy(att.type)}\n </span>\n </div>\n {mine && onRetryAttachment && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"chat-attachment-retry\"\n onClick={onRetryAttachment}\n >\n Attach again\n </Button>\n )}\n </div>\n );\n }\n // ready (or legacy/absent status) — resolve the display URL from the asset ref.\n const url = resolveAttachmentUrl(att);\n if (!url) {\n // Terminal-ready row, but the authorized URL is still SETTLING (the grant/URL\n // resolver has nothing yet). Returning null left only the surrounding message\n // metadata → an observed empty bubble; a broken/grant-less <img> would flash a\n // 403. Render a neutral loading placeholder in the existing loading visual\n // language (spinner + kind-appropriate generic label + \"Loading…\", never\n // \"Sending…\", NO filename) and do NOT mount the media/download renderer until a\n // non-empty authorized URL exists. Sender-only pending/processing/failed states\n // above are unchanged — this begins only after a terminal-ready row is received.\n return (\n <div className=\"chat-attachment-pending\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Loading…</span>\n </div>\n );\n }\n if (att.type === \"image\") {\n return <Media type=\"image\" url={url} alt=\"Image attachment\" className=\"chat-attachment-media\" />;\n }\n if (att.type === \"video\") {\n return <Media type=\"video\" url={url} controls className=\"chat-attachment-media\" />;\n }\n if (att.type === \"audio\") {\n return <Media type=\"audio\" url={url} controls className=\"chat-attachment-media\" />;\n }\n // text/markdown — download link\n return (\n <a\n href={url}\n download={att.name}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"chat-attachment-text-link\"\n >\n <FileText className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">Download attachment</span>\n </a>\n );\n}\n\n// ============================================\n// Reply-to quote\n// ============================================\n\nfunction ReplyQuote({ replyTo }: { replyTo: NonNullable<ChatMessageV1[\"replyTo\"]> }) {\n const replyName = useResolvedSenderName(replyTo.senderId);\n return (\n <div className=\"chat-reply-quote\">\n <span className=\"chat-reply-quote-sender\">{replyName}</span>\n <span className=\"chat-reply-quote-preview\"><MessageText text={replyTo.messagePreview} /></span>\n </div>\n );\n}\n\n// ============================================\n// System message\n// ============================================\n\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n </div>\n );\n}\n\n// ============================================\n// Default message item\n// ============================================\n\nexport type MessageItemDefaultProps = {\n m: ChatMessageV1;\n currentUserId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n\n // Grouping\n isContinuation?: boolean;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n /** Retry a failed realtime send by its original clientMessageId (wired by\n * ChatShell on the realtime transport; absent on firestore). Enables the\n * retry affordance on a `meta.sendFailed` bubble. */\n onRetrySend?: (clientMessageId: string) => void;\n\n /** Re-open the attachment picker for a terminally-failed attachment (wired by\n * ChatShell to the Composer's `openAttachmentSelector`; present only when\n * attachment support is configured). Enables the \"Attach again\" action on a\n * failed attachment bubble, shown only to the sender. */\n onRetryAttachment?: () => void;\n\n};\n\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend, onRetryAttachment } = props;\n const senderName = useResolvedSenderName(m.senderId);\n\n // System messages render differently\n if (m.isSystemMessage) {\n return <SystemMessage m={m} />;\n }\n\n const mine = m.senderId === currentUserId;\n\n // Realtime optimistic-send lifecycle (meta is stamped by the channel client):\n // an un-acked echo renders subtly pending; one past the retry/age cap renders a\n // visibly failed bubble with a retry affordance (same clientMessageId — the DO\n // dedups, so a retry can never double-send).\n const sendFailed = m.meta?.sendFailed === true;\n const sendPending = m.meta?.optimistic === true && !sendFailed;\n const clientMessageId =\n typeof m.meta?.clientMessageId === \"string\" ? m.meta.clientMessageId : null;\n // Correlated rejection reason (from the `send-rejected` frame), if any — drives\n // both the failure copy and whether a Retry affordance is offered.\n const sendFailureCode =\n typeof m.meta?.sendFailureCode === \"string\" ? m.meta.sendFailureCode : null;\n const showRetry = sendFailed && isRetryableFailure(m.meta);\n\n return (\n <div\n className={cn(\n \"flex flex-col w-fit max-w-[85%]\",\n mine ? \"ml-auto items-end\" : \"mr-auto items-start\",\n isContinuation ? \"chat-continuation-gap\" : \"chat-group-gap\"\n )}\n >\n <div\n className={cn(\n \"chat-bubble\",\n mine ? \"chat-bubble--mine\" : \"chat-bubble--theirs\",\n sendPending && \"chat-bubble--pending\",\n sendFailed && \"chat-bubble--failed\"\n )}\n >\n {!isContinuation && (\n <div className=\"flex items-center gap-2 text-xs opacity-80 mb-1\">\n <span\n className={cn(\"font-medium\", onSenderClick && \"cursor-pointer hover:underline\")}\n onClick={onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined}\n role={onSenderClick ? \"button\" : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={onSenderClick ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onSenderClick(m.senderId, senderName);\n } : undefined}\n >\n {senderName}\n </span>\n <span>·</span>\n <span>{new Date(m.createdAt).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}</span>\n </div>\n )}\n\n {m.replyTo && <ReplyQuote replyTo={m.replyTo} />}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\n )}\n\n {m.attachment && (\n <div className=\"mt-2\">\n <AttachmentView att={m.attachment} mine={mine} onRetryAttachment={onRetryAttachment} />\n </div>\n )}\n </div>\n\n {/* Send-state status rows render regardless of grouping — a pending/failed\n send must never be indistinguishable from a delivered one. */}\n {sendPending && (\n <div className=\"chat-send-status mt-0.5\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-3 w-3 shrink-0 animate-spin\" />\n <span>Sending…</span>\n </div>\n )}\n {sendFailed && (\n <div className=\"chat-send-status chat-send-status--failed mt-0.5\" role=\"alert\">\n <AlertTriangle className=\"h-3 w-3 shrink-0\" />\n <span>{sendFailureCopy(sendFailureCode)}</span>\n {showRetry && onRetrySend && clientMessageId && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-5 px-1.5 text-[0.6875rem]\"\n onClick={() => onRetrySend(clientMessageId)}\n >\n Retry\n </Button>\n )}\n </div>\n )}\n\n {!isContinuation && (\n <div className=\"mt-1\">\n <MessageActions messageId={m.messageId} isAdmin={isAdmin} handlers={handlers} />\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -28,5 +28,8 @@ export declare function MessageList(props: {
28
28
  onSenderClick?: (senderId: string, displayName: string) => void;
29
29
  /** Retry a failed realtime send by clientMessageId (realtime transport only). */
30
30
  onRetrySend?: (clientMessageId: string) => void;
31
+ /** Re-open the attachment picker for a terminally-failed attachment. Present only
32
+ * when attachment support is configured; forwarded to each MessageItemDefault. */
33
+ onRetryAttachment?: () => void;
31
34
  }): React.JSX.Element;
32
35
  //# sourceMappingURL=MessageList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/ui/MessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEpF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAI3D,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IAEjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEzB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IACtD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAE3C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B;4FACwF;IACxF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C;;4BAEwB;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;yEAGqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,iFAAiF;IACjF,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD,qBAiMA"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/ui/MessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEpF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAI3D,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IAEjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEzB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IACtD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAE3C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B;4FACwF;IACxF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C;;4BAEwB;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;yEAGqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,iFAAiF;IACjF,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;uFACmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC,qBAmMA"}
@@ -5,7 +5,7 @@ import { isContinuation } from "@ttt-productions/chat-core";
5
5
  import { Button } from "@ttt-productions/ui-core/react";
6
6
  import { MessageItemDefault } from "./MessageItemDefault.js";
7
7
  export function MessageList(props) {
8
- const { messages, currentUserId, isAdmin, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, fillHeight = false, handlers, onSenderClick, onRetrySend, } = props;
8
+ const { messages, currentUserId, isAdmin, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, fillHeight = false, handlers, onSenderClick, onRetrySend, onRetryAttachment, } = props;
9
9
  const scrollClass = scrollClassName ?? (fillHeight ? "flex-1 min-h-0" : "h-[400px]");
10
10
  const outerClass = fillHeight ? "relative flex flex-col flex-1 min-h-0" : "relative";
11
11
  const scrollRef = React.useRef(null);
@@ -107,7 +107,7 @@ export function MessageList(props) {
107
107
  const continuation = showDay ? false : isContinuation(prevMsg, m);
108
108
  const byType = m.type && messageRenderers?.[m.type] ? messageRenderers[m.type](m) : null;
109
109
  const body = renderMessage?.(m) ??
110
- byType ?? (_jsx(MessageItemDefault, { m: m, currentUserId: currentUserId, isAdmin: isAdmin, handlers: handlers, isContinuation: continuation, onSenderClick: onSenderClick, onRetrySend: onRetrySend }));
110
+ byType ?? (_jsx(MessageItemDefault, { m: m, currentUserId: currentUserId, isAdmin: isAdmin, handlers: handlers, isContinuation: continuation, onSenderClick: onSenderClick, onRetrySend: onRetrySend, onRetryAttachment: onRetryAttachment }));
111
111
  return (_jsxs(React.Fragment, { children: [showDay && (_jsx("div", { className: "my-2 flex justify-center", children: _jsx("div", { className: "chat-date-separator", children: new Date(m.createdAt).toLocaleDateString() }) })), body] }, m.messageId));
112
112
  }) }))] }), (unseenCount > 0 || showScrollToBottom) && (_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": unseenCount > 0 ? `${unseenCount} new messages, scroll to latest` : "Scroll to latest", className: "absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full shadow-sm gap-1", onClick: () => {
113
113
  scrollToBottom();