import { useState, useRef, useEffect, memo } from "react"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { useProfile } from "../../hooks/useProfile"; import { useNip05Verified } from "../../hooks/useNip05Verified"; import { useUserStore } from "../../stores/user"; import { useMuteStore } from "../../stores/mute"; import { useUIStore } from "../../stores/ui"; import { timeAgo, shortenPubkey } from "../../lib/utils"; import { getNDK, fetchNoteById, ensureConnected } from "../../lib/nostr"; import { getParentEventId } from "../../lib/threadTree"; import { NoteContent } from "./NoteContent"; import { NoteActions, LoggedOutStats } from "./NoteActions"; import { InlineReplyBox } from "./InlineReplyBox"; interface NoteCardProps { event: NDKEvent; focused?: boolean; onReplyInThread?: (event: NDKEvent) => void; } function ParentAuthorName({ pubkey }: { pubkey: string }) { const profile = useProfile(pubkey); const raw = profile?.displayName || profile?.name; const name = (typeof raw === "string" ? raw : null) || pubkey.slice(0, 8) + "…"; return @{name}; } export const NoteCard = memo(function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) { const profile = useProfile(event.pubkey); const rawName = profile?.displayName || profile?.name; const name = (typeof rawName === "string" ? rawName : null) || shortenPubkey(event.pubkey); const avatar = profile?.picture; const nip05 = typeof profile?.nip05 === "string" ? profile.nip05 : null; const verified = useNip05Verified(event.pubkey, nip05); const time = event.created_at ? timeAgo(event.created_at) : ""; const loggedIn = useUserStore((s) => s.loggedIn); const ownPubkey = useUserStore((s) => s.pubkey); const follows = useUserStore((s) => s.follows); const follow = useUserStore((s) => s.follow); const unfollow = useUserStore((s) => s.unfollow); const mutedPubkeys = useMuteStore((s) => s.mutedPubkeys); const mute = useMuteStore((s) => s.mute); const unmute = useMuteStore((s) => s.unmute); const isMuted = mutedPubkeys.includes(event.pubkey); const openProfile = useUIStore((s) => s.openProfile); const openThread = useUIStore((s) => s.openThread); const currentView = useUIStore((s) => s.currentView); const parentEventId = getParentEventId(event); // The immediate parent author is typically the last p tag (NIP-10 ordering mirrors e tags). // First p tag is usually the root author, not who this note directly replies to. const pTags = event.tags.filter((t) => t[0] === "p"); const parentAuthorPubkey = pTags.length > 0 ? pTags[pTags.length - 1][1] : null; const cardRef = useRef(null); useEffect(() => { if (focused) cardRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }); }, [focused]); const [menuOpen, setMenuOpen] = useState(false); const [showReply, setShowReply] = useState(false); return (
{ // Don't navigate if clicking on interactive elements const target = e.target as HTMLElement; if (target.closest("button, a, input, textarea, [data-no-navigate]")) return; openThread(event, currentView as "feed" | "profile"); }} >
{/* Avatar */} {/* Content */}
{nip05 && ( {verified === "valid" ? "✓ " : ""}{nip05} )} {time} {/* Context menu — hidden until card hover, not shown for own notes */} {loggedIn && event.pubkey !== ownPubkey && (
{menuOpen && ( <>
setMenuOpen(false)} />
)}
)}
{parentEventId && parentAuthorPubkey && (
)}
{/* Actions */} {loggedIn && !!getNDK().signer && ( { if (onReplyInThread) { onReplyInThread(event); } else { setShowReply((v) => !v); } }} showReply={showReply && !onReplyInThread} /> )} {/* Stats visible when logged out */} {!loggedIn && } {/* Inline reply box */} {showReply && }
); });