Add nested thread trees, recursive reply fetching, multi-level back navigation

Overhauls the thread view from flat single-level replies to proper nested
conversation trees. Fixes NIP-10 tagging (root + reply markers), adds
2-round-trip recursive thread fetch, ancestor chain display, reply-to-any-note
targeting, view stack navigation (up to 20 levels), and loading shimmer.
This commit is contained in:
Jure
2026-03-21 15:21:46 +01:00
parent 5a8250e7cf
commit acb0d531c0
10 changed files with 578 additions and 236 deletions
+60
View File
@@ -0,0 +1,60 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useProfile } from "../../hooks/useProfile";
import { useUIStore } from "../../stores/ui";
import { shortenPubkey, timeAgo } from "../../lib/utils";
function AncestorCard({ event }: { event: NDKEvent }) {
const profile = useProfile(event.pubkey);
const name = profile?.displayName || profile?.name || shortenPubkey(event.pubkey);
const avatar = profile?.picture;
const time = event.created_at ? timeAgo(event.created_at) : "";
const { openThread } = useUIStore();
const truncated = event.content.length > 120
? event.content.slice(0, 120) + "..."
: event.content;
return (
<button
onClick={() => openThread(event)}
className="w-full text-left px-4 py-2 border-b border-border hover:bg-bg-hover transition-colors flex gap-2.5 items-start"
>
<div className="shrink-0 mt-0.5">
{avatar ? (
<img src={avatar} alt="" className="w-6 h-6 rounded-sm object-cover bg-bg-raised" loading="lazy" />
) : (
<div className="w-6 h-6 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-[9px]">
{name.charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2 mb-0.5">
<span className="text-text font-medium text-[11px] truncate">{name}</span>
<span className="text-text-dim text-[10px] shrink-0">{time}</span>
</div>
<div className="text-text-dim text-[11px] line-clamp-2 break-words">{truncated}</div>
</div>
<div className="text-text-dim text-[10px] shrink-0 mt-0.5"></div>
</button>
);
}
interface AncestorChainProps {
ancestors: NDKEvent[];
}
export function AncestorChain({ ancestors }: AncestorChainProps) {
if (ancestors.length === 0) return null;
return (
<div className="bg-bg-raised/50">
<div className="px-4 py-1.5 text-text-dim text-[10px] border-b border-border">
{ancestors.length} parent {ancestors.length === 1 ? "note" : "notes"} above
</div>
{ancestors.map((a) => (
<AncestorCard key={a.id} event={a} />
))}
</div>
);
}