Instant thread display, faster fetches, trending always shows notes

Threads now render the focused note immediately instead of showing a
loading skeleton. Root + ancestors fetch in parallel, timeouts cut
from 10s to 5s per round-trip, ancestor lookups from 5s to 2s.

Trending feed adds a base recency score so notes always appear even
when engagement data times out from relays.
This commit is contained in:
Jure
2026-03-29 17:50:10 +02:00
parent 8e685f2d4b
commit 2bb1341eed
4 changed files with 27 additions and 23 deletions

View File

@@ -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);
}
}

View File

@@ -14,7 +14,7 @@ export function withTimeout<T>(promise: Promise<T>, 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<NDKEvent>();

View File

@@ -148,6 +148,8 @@ export async function fetchThreadEvents(rootId: string): Promise<NDKEvent[]> {
return Array.from(allEvents.values());
}
const ANCESTOR_TIMEOUT = 2000; // 2s per parent — fail fast
export async function fetchAncestors(event: NDKEvent, maxDepth = 5): Promise<NDKEvent[]> {
const ancestors: NDKEvent[] = [];
let current = event;
@@ -162,7 +164,10 @@ export async function fetchAncestors(event: NDKEvent, maxDepth = 5): Promise<NDK
eTags[eTags.length - 1][1];
if (!parentId) break;
const parent = await fetchNoteById(parentId);
const instance = getNDK();
const filter: NDKFilter = { ids: [parentId], limit: 1 };
const events = await fetchWithTimeout(instance, filter, ANCESTOR_TIMEOUT);
const parent = Array.from(events)[0] ?? null;
if (!parent) break;
ancestors.unshift(parent);
current = parent;

View File

@@ -246,10 +246,11 @@ export const useFeedStore = create<FeedState>((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);