import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { X } from 'lucide-react'; import type { NostrEvent } from '@nostrify/nostrify'; import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog'; import { PortalContainerProvider } from '@/hooks/usePortalContainer'; import { EmbeddedPost } from '@/components/EmbeddedPost'; import { ComposeBox, type ExternalReplyRoot } from '@/components/ComposeBox'; import { LinkEmbed } from '@/components/LinkEmbed'; import { cn } from '@/lib/utils'; interface ReplyComposeModalProps { /** The event being replied to, a URL for commenting on web content, or a NIP-73 identifier (e.g. `bitcoin:tx:...`, `isbn:...`). When `null`, the modal acts as a "New post" composer. */ event?: NostrEvent | ExternalReplyRoot | null; /** The event being quoted (for quote posts). */ quotedEvent?: NostrEvent | null; open: boolean; onOpenChange: (open: boolean) => void; /** Called after a post is successfully published. */ onSuccess?: () => void; /** Pre-filled content for the compose box. */ initialContent?: string; /** Open directly in poll mode. */ initialMode?: 'post' | 'poll'; /** Override the modal title. */ title?: string; /** Override the compose box placeholder text. */ placeholder?: string; } export function ReplyComposeModal({ event, quotedEvent, open, onOpenChange, onSuccess, initialContent, initialMode, title: titleOverride, placeholder: placeholderOverride }: ReplyComposeModalProps) { const { t } = useTranslation(); const isUrl = event instanceof URL; const isExternalId = typeof event === 'string'; const isExternal = isUrl || isExternalId; const isReply = !!event; const isQuote = !!quotedEvent; const [previewMode, setPreviewMode] = useState(false); const [hasPreviewableContent, setHasPreviewableContent] = useState(false); const [portalContainer, setPortalContainer] = useState(undefined); const isProfileRoot = !isExternal && event instanceof Object && 'kind' in event && event.kind === 0; const title = titleOverride ?? ( initialMode === 'poll' ? t('replyModal.title.newPoll') : isExternal ? t('replyModal.title.newComment') : isProfileRoot ? t('replyModal.title.commentOnProfile') : isReply ? t('replyModal.title.replyToPost') : isQuote ? t('replyModal.title.quotePost') : t('replyModal.title.newPost') ); const placeholder = placeholderOverride ?? ( isExternal ? t('replyModal.placeholder.writeComment') : isReply ? t('compose.placeholderDefault') : isQuote ? t('replyModal.placeholder.addComment') : t('replyModal.placeholder.whatsHappening') ); const dialogContentRef = useCallback((node: HTMLElement | null) => { setPortalContainer(node ?? undefined); }, []); // Always prevent closing the compose modal by clicking the backdrop overlay. // Users must explicitly close via the X button or Escape key. This prevents // accidental content loss from stray taps on mobile or misclicks. const handleInteractOutside = useCallback((e: Event) => { e.preventDefault(); }, []); return ( { // Prevent Radix from focusing its own first-focusable (the X button). e.preventDefault(); // Immediately focus the textarea — this MUST happen synchronously // inside this handler so iOS treats it as part of the original user // gesture and raises the virtual keyboard. const textarea = (e.currentTarget as HTMLElement).querySelector('textarea'); if (textarea) { textarea.focus(); } }} onInteractOutside={handleInteractOutside} > {/* Header */}
{title}
{/* Preview toggle */} {hasPreviewableContent && (
)}
{/* Embedded original post (reply only, not for quotes) Capped at 20% of viewport so it never dominates the modal. */} {event && !isQuote && (
{isUrl ? (
) : isExternalId ? ( /* NIP-73 identifier root — no rich preview; the page context already shows it. */ null ) : ( )}
)} {/* Bluesky disclaimer */} {isUrl && /bsky\.(app|social)/.test(event.href) && (
⚠️

{t('replyModal.blueskyDisclaimer')}

)} {/* Compose area — takes remaining space; ComposeBox handles its own scroll */}
{ onOpenChange(false); onSuccess?.(); }} placeholder={placeholder} forceExpanded hideAvatar previewMode={previewMode} onHasPreviewableContentChange={setHasPreviewableContent} initialContent={initialContent} initialMode={initialMode} />
); }