153 lines
5.6 KiB
TypeScript
Raw Normal View History

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 { 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, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { MessageSquare, ChevronDown, ChevronRight, MoreHorizontal } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { genUserName } from '@/lib/genUserName';
interface CommentProps {
root: NostrEvent | URL;
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 author = useAuthor(comment.pubkey);
const { data: commentsData } = useComments(root, limit);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(comment.pubkey)
const timeAgo = formatDistanceToNow(new Date(comment.created_at * 1000), { addSuffix: true });
// Get direct replies to this comment
const replies = commentsData?.getDirectReplies(comment.id) || [];
const hasReplies = replies.length > 0;
return (
<div className={`space-y-3 ${depth > 0 ? 'ml-6 border-l-2 border-muted pl-4' : ''}`}>
<Card className="bg-card/50">
<CardContent className="p-4">
<div className="space-y-3">
{/* Comment Header */}
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
<Link to={`/${nip19.npubEncode(comment.pubkey)}`}>
<Avatar className="h-8 w-8 hover:ring-2 hover:ring-primary/30 transition-all cursor-pointer">
<AvatarImage src={metadata?.picture} />
<AvatarFallback className="text-xs">
{displayName.charAt(0)}
</AvatarFallback>
</Avatar>
</Link>
<div>
<Link
to={`/${nip19.npubEncode(comment.pubkey)}`}
className="font-medium text-sm hover:text-primary transition-colors"
>
{displayName}
</Link>
<p className="text-xs text-muted-foreground">{timeAgo}</p>
</div>
</div>
</div>
{/* Comment Content */}
<div className="text-sm">
<NoteContent event={comment} className="text-sm" />
</div>
{/* Comment Actions */}
<div className="flex items-center justify-between pt-2">
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => setShowReplyForm(!showReplyForm)}
className="h-8 px-2 text-xs"
>
<MessageSquare className="h-3 w-3 mr-1" />
Reply
</Button>
{hasReplies && (
<Collapsible open={showReplies} onOpenChange={setShowReplies}>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 px-2 text-xs">
{showReplies ? (
<ChevronDown className="h-3 w-3 mr-1" />
) : (
<ChevronRight className="h-3 w-3 mr-1" />
)}
{replies.length} {replies.length === 1 ? 'reply' : 'replies'}
</Button>
</CollapsibleTrigger>
</Collapsible>
)}
</div>
{/* Comment menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs"
aria-label="Comment options"
>
<MoreHorizontal className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
</DropdownMenu>
</div>
</div>
</CardContent>
</Card>
{/* Reply Form */}
{showReplyForm && (
<div className="ml-6">
<CommentForm
root={root}
reply={comment}
onSuccess={() => setShowReplyForm(false)}
placeholder="Write a reply..."
compact
/>
</div>
)}
{/* Replies */}
{hasReplies && (
<Collapsible open={showReplies} onOpenChange={setShowReplies}>
<CollapsibleContent className="space-y-3">
{replies.map((reply) => (
<Comment
key={reply.id}
root={root}
comment={reply}
depth={depth + 1}
maxDepth={maxDepth}
limit={limit}
/>
))}
</CollapsibleContent>
</Collapsible>
)}
</div>
);
}