import React from 'react' import Bubble from '../_shared/Bubble' import CompactDocumentRow from '../_shared/CompactDocumentRow' import DismissButton from '../_shared/DismissButton' import DownloadAction from '../_shared/DownloadAction' import { filenameFromUrl } from '../_shared/fileMeta' import PdfViewer, { type PdfViewerItem } from '../_shared/PdfViewer' import { useViewer } from '../_shared/useViewer' import { bubbleVariantForState, type BubbleVariant, type ComposerExtras, type MessageAttachmentBaseProps, type MessageAttachmentState, type PdfItem, } from '../types' export interface PdfAttachmentSharedProps extends MessageAttachmentBaseProps { /** Single PDF — convenience for the most common case. */ src?: string /** Filename — drives the row title + the meta line + the viewer dialog's accessible name. */ filename?: string /** File size in bytes — formatted into the `EXT · SIZE` meta line. */ fileSize?: number /** * Override displayed title (defaults to `filename`). Useful when a * sender re-titles a generated PDF before sending. */ title?: string /** * Stacked PDFs. Takes precedence over `src` when set. Each item * renders as its own row inside the bubble; clicking a row opens * that PDF in the viewer. Sent + Received only — the composer * surface accepts a single attachment at a time. */ items?: PdfItem[] /** * Forwarded to the viewer trigger. When omitted the click still * opens the built-in `PdfViewer`. Supply this to fire analytics, or * return `false` to intercept the open (e.g. swap to a route-based * viewer / confirmation modal). Any other return value (including * `void`/`undefined`) lets the built-in viewer open. For stacked * attachments the `index` argument identifies the row. */ onClick?: (index: number) => boolean | void } const resolveItems = ({ src, filename, fileSize, title, items, }: { src?: string filename?: string fileSize?: number title?: string items?: PdfItem[] }): PdfItem[] => { if (items && items.length > 0) return items if (src) return [{ src, filename, fileSize, title }] return [] } interface PdfRowButtonProps { variant: BubbleVariant item: PdfItem index: number onActivate: (index: number) => void trailingAction?: React.ReactNode } /** * Single tappable PDF row — used both for the lone PDF case and for * each child of a stacked attachment. The icon + filename area opens * the viewer; the trailing slot hosts a separate, sibling button * (download or dismiss) so each affordance owns its own click target. */ const PdfRowButton: React.FC = ({ variant, item, index, onActivate, trailingAction, }) => { const resolvedFilename = item.filename ?? filenameFromUrl(item.src) return ( onActivate(index)} activateLabel={`Open ${resolvedFilename}`} trailingAction={trailingAction} /> ) } interface InternalPdfRowProps extends PdfAttachmentSharedProps { state: MessageAttachmentState onDismiss?: () => void } const PdfAttachmentRow: React.FC = ({ state, src, filename, fileSize, title, items, text, groupPosition, onClick, onDismiss, }) => { const variant = bubbleVariantForState(state) const resolvedItems = resolveItems({ src, filename, fileSize, title, items }) const { viewerOpen, viewerIndex, handleActivate, closeViewer } = useViewer(onClick) const showDismiss = state === 'composer' && !!onDismiss if (resolvedItems.length === 0) { return null } // Project the bubble's `PdfItem`s onto the viewer's carousel-aware // shape. The viewer clamps its own active index defensively, so we // don't need to second-guess `viewerIndex` here. const viewerItems: PdfViewerItem[] = resolvedItems.map((item) => ({ src: item.src, filename: item.filename ?? filenameFromUrl(item.src), })) return ( {/* Stacked rows get a small vertical gap so each PDF reads as a discrete attachment rather than one merged blob. Single row falls through to a no-op `gap-0`. */}
{resolvedItems.map((item, index) => { const itemFilename = item.filename ?? filenameFromUrl(item.src) // Composer only supports a single attachment so the dismiss // control sits on the only row. Sent / Received rows expose // a download icon button so the user can grab the PDF // without opening the viewer first — the viewer dialog is // intentionally just the document + a close button, so this // sibling action is the only one-click download surface. const trailingAction = showDismiss && index === 0 ? ( ) : state === 'composer' ? undefined : ( ) return ( ) })}
) } /** * Composer-only props. Single PDF (`src`) only — the composer surface * accepts a single attachment at a time, so `items` is not supported. * Captions (`text`) and `groupPosition` are also dropped: the composer * renders a standalone draft, not part of a same-author run, and * captions live in the message-input textarea, not inside the draft * attachment preview. */ export interface PdfComposerProps extends ComposerExtras { src?: string filename?: string fileSize?: number title?: string onClick?: (index: number) => boolean | void } export type PdfSentProps = PdfAttachmentSharedProps export type PdfReceivedProps = PdfAttachmentSharedProps const PdfComposer: React.FC = (props) => ( ) const PdfSent: React.FC = (props) => ( ) const PdfReceived: React.FC = (props) => ( ) const PdfAttachment = { Composer: PdfComposer, Sent: PdfSent, Received: PdfReceived, } export default PdfAttachment