diff --git a/src/components/thread/ThreadView.tsx b/src/components/thread/ThreadView.tsx index 4ca804d..a7da0c7 100644 --- a/src/components/thread/ThreadView.tsx +++ b/src/components/thread/ThreadView.tsx @@ -40,36 +40,42 @@ export function ThreadView() { let cancelled = false; async function loadThread() { - setLoading(true); setLoadError(null); setTree(null); setAncestors([]); setRootEvent(null); setShowRootReply(false); + // Show focused note immediately as a minimal tree (no waiting) + const minimalTree = buildThreadTree(focusedEvent.id, [focusedEvent]); + setRootEvent(focusedEvent); + setTree(minimalTree); + setLoading(false); + try { // Ensure we have relay connectivity before fetching const connected = await ensureConnected(); if (!connected && !cancelled) { setLoadError("No relay connections available. Check your network."); + return; } const rootId = getRootEventId(focusedEvent); - let root: NDKEvent; + let root: NDKEvent = focusedEvent; let fetchedAncestors: NDKEvent[] = []; - if (!rootId || rootId === focusedEvent.id) { - root = focusedEvent; - } else { - const fetched = await fetchNoteById(rootId); + if (rootId && rootId !== focusedEvent.id) { + // Fetch root and ancestors in parallel with thread replies + const [fetched, ancestorResult] = await Promise.all([ + fetchNoteById(rootId), + fetchAncestors(focusedEvent), + ]); if (fetched) { root = fetched; - fetchedAncestors = await fetchAncestors(focusedEvent); - fetchedAncestors = fetchedAncestors.filter((a) => a.id !== root.id); + fetchedAncestors = ancestorResult.filter((a) => a.id !== root.id); if (!cancelled) setAncestors(fetchedAncestors); - } else { - root = focusedEvent; - if (!cancelled) setLoadError("Could not fetch the root note — relay may be slow."); + } else if (!cancelled) { + setLoadError("Could not fetch the root note — relay may be slow."); } } @@ -80,7 +86,6 @@ export function ThreadView() { if (cancelled) return; // Build event list: root + thread replies + focused event + ancestors - // Always include focused event — relay may not return it in thread fetch const allEvents = [root, ...events.filter((e) => e.id !== root.id)]; if (focusedEvent.id !== root.id && !allEvents.some((e) => e.id === focusedEvent.id)) { allEvents.push(focusedEvent); @@ -93,16 +98,9 @@ export function ThreadView() { const built = buildThreadTree(root.id, allEvents); setTree(built); - - // Warn if we got zero replies (possible timeout) - if (events.length === 0 && !loadError) { - console.warn("[Wrystr] Thread fetch returned 0 replies — possible timeout or empty thread"); - } } catch (err) { console.error("Failed to load thread:", err); if (!cancelled) setLoadError(`Failed to load: ${err}`); - } finally { - if (!cancelled) setLoading(false); } } diff --git a/src/lib/nostr/core.ts b/src/lib/nostr/core.ts index 0e516fd..efda371 100644 --- a/src/lib/nostr/core.ts +++ b/src/lib/nostr/core.ts @@ -14,7 +14,7 @@ export function withTimeout(promise: Promise, ms: number, fallback: T): Pr } export const FEED_TIMEOUT = 8000; // 8s for feed fetches -export const THREAD_TIMEOUT = 10000; // 10s per thread round-trip +export const THREAD_TIMEOUT = 5000; // 5s per thread round-trip export const SINGLE_TIMEOUT = 5000; // 5s for single event lookups const EMPTY_SET = new Set(); diff --git a/src/lib/nostr/notes.ts b/src/lib/nostr/notes.ts index 4981a5d..806a5d7 100644 --- a/src/lib/nostr/notes.ts +++ b/src/lib/nostr/notes.ts @@ -148,6 +148,8 @@ export async function fetchThreadEvents(rootId: string): Promise { return Array.from(allEvents.values()); } +const ANCESTOR_TIMEOUT = 2000; // 2s per parent — fail fast + export async function fetchAncestors(event: NDKEvent, maxDepth = 5): Promise { const ancestors: NDKEvent[] = []; let current = event; @@ -162,7 +164,10 @@ export async function fetchAncestors(event: NDKEvent, maxDepth = 5): Promise((set, get) => ({ const eng = engagement.get(note.id) ?? { reactions: 0, replies: 0, zapSats: 0 }; const ageHours = (now - (note.created_at ?? now)) / 3600; const decay = 1 / (1 + ageHours * 0.15); - const score = (eng.reactions * 1 + eng.replies * 3 + eng.zapSats * 0.01) * decay; + const engScore = eng.reactions * 1 + eng.replies * 3 + eng.zapSats * 0.01; + // Base recency score ensures notes appear even when engagement data times out + const score = (engScore + 0.1) * decay; return { note, score }; }) - .filter((s) => s.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 50) .map((s) => s.note);