import { useState } from 'react'; import { Link } from 'react-router-dom'; import { NostrEvent } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import { useAuthor } from '@/hooks/useAuthor'; import { useComments } from '@/hooks/useComments'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useToast } from '@/hooks/useToast'; import { useQueryClient } from '@tanstack/react-query'; import { CommentForm } from './CommentForm'; import { NoteContent } from '@/components/NoteContent'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { MessageSquare, ChevronDown, ChevronRight, MoreHorizontal, ExternalLink, Trash2, } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; interface CommentProps { root: NostrEvent | URL | `#${string}`; comment: NostrEvent; depth?: number; maxDepth?: number; limit?: number; } export function Comment({ root, comment, depth = 0, maxDepth = 3, limit }: CommentProps) { const [showReplyForm, setShowReplyForm] = useState(false); const [showReplies, setShowReplies] = useState(depth < 2); // Auto-expand first 2 levels const [showDeleteDialog, setShowDeleteDialog] = useState(false); const author = useAuthor(comment.pubkey); const { data: commentsData } = useComments(root, limit); const { user } = useCurrentUser(); const { mutate: publishEvent, isPending: isDeleting } = useNostrPublish(); const { toast } = useToast(); const queryClient = useQueryClient(); const metadata = author.data?.metadata; const displayName = metadata?.name ?? 'Anonymous'; const timeAgo = formatDistanceToNow(new Date(comment.created_at * 1000), { addSuffix: true }); const nevent = nip19.neventEncode({ id: comment.id, author: comment.pubkey, kind: comment.kind }); const isOwnComment = user?.pubkey === comment.pubkey; // Get direct replies to this comment const replies = commentsData?.getDirectReplies(comment.id) || []; const hasReplies = replies.length > 0; const handleDelete = () => { publishEvent( { kind: 5, content: '', tags: [ ['e', comment.id], ['k', comment.kind.toString()], ], }, { onSuccess: () => { toast({ title: 'Comment deleted' }); setShowDeleteDialog(false); // Optimistically remove the deleted comment from every cached useComments // result. Relays may honor the kind 5 deletion request on refetch, but // that's not guaranteed — this keeps the UI consistent immediately. queryClient.setQueriesData['data']>( { queryKey: ['nostr', 'comments'] }, (oldData) => { if (!oldData) return oldData; const allComments = oldData.allComments.filter((c) => c.id !== comment.id); const topLevelComments = oldData.topLevelComments.filter( (c) => c.id !== comment.id, ); return { ...oldData, allComments, topLevelComments, getDescendants: (id: string) => oldData.getDescendants(id).filter((c) => c.id !== comment.id), getDirectReplies: (id: string) => oldData.getDirectReplies(id).filter((c) => c.id !== comment.id), }; }, ); }, onError: (error) => { toast({ title: 'Failed to delete comment', description: error.message, variant: 'destructive', }); }, }, ); }; return (
0 ? 'ml-6 border-l-2 border-muted pl-4' : ''}`}>
{/* Comment Header */}
{displayName.charAt(0)}
{displayName}

{timeAgo}

{/* Comment Content */}
{/* Comment Actions */}
{hasReplies && ( )}
{/* Comment menu */} Open in Ditto {isOwnComment && ( <> { e.preventDefault(); setShowDeleteDialog(true); }} className="text-destructive focus:text-destructive" > Delete )}
{/* Reply Form */} {showReplyForm && (
setShowReplyForm(false)} placeholder="Write a reply..." compact />
)} {/* Replies */} {hasReplies && ( {replies.map((reply) => ( ))} )} {/* Delete confirmation */} Delete comment? This publishes a NIP-09 deletion request (kind 5). Relays and clients that honor deletion requests will hide this comment, but deletion is not guaranteed across the network. Cancel { e.preventDefault(); handleDelete(); }} disabled={isDeleting} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {isDeleting ? 'Deleting…' : 'Delete'}
); }