/** * ReplyCard - Displays a single reply. * Includes reactions, report button, and inline editing for authors. * Indentation is structural via nested
    in ReplyBranch. * Deleted replies render as tombstone placeholders. * @see specs/prd-web.md Section 4 (Topic Components) */ 'use client' import { useState, useCallback } from 'react' import Link from 'next/link' import Image from 'next/image' import { Clock, Link as LinkIcon, ChatCircle, PencilSimple, Trash } from '@phosphor-icons/react' import type { Reply } from '@/lib/api/types' import { formatRelativeTime, isEdited } from '@/lib/format' import { updateReply, deleteReply } from '@/lib/api/client' import { useAuth } from '@/hooks/use-auth' import { useToast } from '@/hooks/use-toast' import { MarkdownContent } from './markdown-content' import { MarkdownEditor } from './markdown-editor' import { LikeButton } from './like-button' import { ReactionBar } from './reaction-bar' import { ReportDialog, type ReportSubmission } from './report-dialog' import { ConfirmDialog } from './confirm-dialog' import { SelfLabelIndicator } from './self-label-indicator' import { PluginSlot } from '@/components/plugin-slot' interface ReactionData { type: string count: number reacted: boolean } interface ReplyCardProps { reply: Reply postNumber: number reactions?: ReactionData[] onReactionToggle?: (type: string) => void onReply?: (target: { uri: string; cid: string; authorHandle: string; snippet: string }) => void canEdit?: boolean canDelete?: boolean onDelete?: () => void canReport?: boolean onReport?: (report: ReportSubmission) => void isOwnContent?: boolean selfLabels?: string[] className?: string } export function ReplyCard({ reply, postNumber, reactions, onReactionToggle, onReply, canEdit, canDelete, onDelete, canReport, onReport, isOwnContent, selfLabels, className, }: ReplyCardProps) { const [isEditingReply, setIsEditingReply] = useState(false) const [editContent, setEditContent] = useState(reply.content) const [displayContent, setDisplayContent] = useState(reply.content) const [saving, setSaving] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const { getAccessToken } = useAuth() const { toast } = useToast() const handleSaveEdit = useCallback(async () => { const trimmed = editContent.trim() if (!trimmed) return setSaving(true) try { const accessToken = getAccessToken() ?? '' await updateReply(reply.uri, { content: trimmed }, accessToken) setDisplayContent(trimmed) setIsEditingReply(false) toast({ title: 'Reply updated' }) } catch (err) { const message = err instanceof Error ? err.message : 'Failed to update reply' toast({ title: 'Error', description: message, variant: 'destructive' }) } finally { setSaving(false) } }, [editContent, reply.uri, getAccessToken, toast]) const handleConfirmDelete = useCallback(async () => { try { const accessToken = getAccessToken() ?? '' await deleteReply(reply.uri, accessToken) setShowDeleteConfirm(false) toast({ title: 'Reply deleted' }) onDelete?.() } catch (err) { const message = err instanceof Error ? err.message : 'Failed to delete reply' toast({ title: 'Error', description: message, variant: 'destructive' }) } }, [reply.uri, getAccessToken, toast, onDelete]) const headingId = `reply-heading-${reply.rkey}` const isDeleted = reply.isAuthorDeleted || reply.isModDeleted if (isDeleted) { const tombstoneText = reply.isModDeleted ? 'This post was removed by a moderator.' : 'This post was removed by the author.' return (
    ) } return (
    {/* Header */}
    {reply.author?.avatarUrl ? ( ) : ( )}

    {reply.author?.displayName ?? reply.author?.handle ?? reply.authorDid}

    {isEdited(reply.createdAt, reply.indexedAt) && ( (edited) )}
    #{postNumber}
    {/* Content */}
    {isEditingReply ? (
    ) : selfLabels && selfLabels.length > 0 ? ( ) : ( )}
    {/* Plugin slot for post content extensions (e.g., signatures) */} {/* TODO: Compute isFirstByAuthor properly when signatures plugin is built */} {/* Footer */}
    {reactions && onReactionToggle && ( )} {canEdit && !isEditingReply && ( )} {canDelete && ( )} {onReply && ( )} {canReport && onReport && }
    setShowDeleteConfirm(false)} />
    ) }