diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 48 | ||||
| -rw-r--r-- | src/nostr.js | 27 |
2 files changed, 59 insertions, 16 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index a378f1d..b3f40f2 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -3495,7 +3495,11 @@ export function messagesFeature(ctx) { relayListCache.set(pk, p); return p; } - const notesRelays = async (pk) => [...new Set([...NOTE_RELAYS, ...(await relaysOf(pk))])]; + // Plus relays that keep history: the big ones prune within days, and an + // author without a NIP-65 list (Joe Nakamoto, 2026-10-01) otherwise shows + // only whatever primal still holds — 2 posts of 22. + const PROFILE_ARCHIVE_RELAYS = ['wss://relay.ditto.pub', 'wss://nostr21.com', 'wss://offchain.pub']; + const notesRelays = async (pk) => [...new Set([...NOTE_RELAYS, ...(await relaysOf(pk)), ...PROFILE_ARCHIVE_RELAYS])]; // Posts of theirs we already hold. Tapping a face in the feed is the // common way onto a profile, and the feed we just came from is full of @@ -3513,35 +3517,51 @@ export function messagesFeature(ctx) { return [...out.values()].sort((a, b) => b.created_at - a.created_at); } + // Asked again when the page is reopened after this long: one bad answer + // (a relay down, the phone just back from sleep) must not freeze a + // profile at two posts for the rest of the session. + const NOTES_REFRESH_MS = 60_000; function notesFor(pk) { let c = notesCache.get(pk); - if (c) return c; + if (c) { + if (!c.fetching && Date.now() - (c.fetchedAt || 0) > NOTES_REFRESH_MS) fetchNotes(pk, c); + return c; + } // stored posts paint the page instantly; the relay fetch below freshens const stored = pageCache().notes[pk]; const seed = stored ? stored.v : notesInHand(pk); // Seeded from memory the status stays 'loading': these are posts we - // happen to have, not their page, so the real answer still replaces - // them — but there is something to read while it comes. + // happen to have, not their page, so there is something to read while + // the real answer comes. c = stored ? { status: 'ready', notes: stored.v } : { status: 'loading', notes: seed }; notesCache.set(pk, c); + fetchNotes(pk, c); + return c; + } + function fetchNotes(pk, c) { + c.fetching = true; (async () => { const evs = await queryOn(await notesRelays(pk), { kinds: FEED_KINDS, authors: [pk], limit: 30 }, 4500); - const seen = new Set(); - const fresh = (evs || []) - .filter((e) => !seen.has(e.id) && seen.add(e.id)) - .sort((a, b) => b.created_at - a.created_at); - if (fresh.length || !c.notes.length) { - await notesReady(fresh.slice(0, FEED_PAGE)); // the first screen arrives whole - c.notes = fresh; - persistPage('notes', pk, fresh.slice(0, 20).map(slimNote)); // cache stays bounded - } + // Merged with what's already on the page, never swapped for it: the + // relays answer unevenly (primal held 2 of Joe Nakamoto's posts, ours + // 18, the archives 30), and a thin answer used to erase a full page. + const byId = new Map(c.notes.map((e) => [e.id, e])); + let added = 0; + for (const e of evs || []) if (!byId.has(e.id)) { byId.set(e.id, e); added++; } + if (!added && c.notes.length) return; + const merged = [...byId.values()].sort((a, b) => b.created_at - a.created_at); + await notesReady(merged.slice(0, FEED_PAGE)); // the first screen arrives whole + c.notes = merged; + c.end = false; + persistPage('notes', pk, merged.slice(0, 20).map(slimNote)); // cache stays bounded })().catch(() => {}).finally(() => { c.status = 'ready'; + c.fetching = false; + c.fetchedAt = Date.now(); if (ui.profilePk === pk) render(); }); - return c; } // Infinite scroll: pull the next page of OLDER notes (until = oldest seen) diff --git a/src/nostr.js b/src/nostr.js index 30ddf85..dcf4bbc 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -117,6 +117,7 @@ const SICK_MS = 10 * 60_000; const SICK_MAX_MS = 24 * 60 * 60_000; const SICK_KEY = 'coinos-sick-relays'; const relayKey = (u) => { try { return normalizeURL(u); } catch { return u; } }; +const HOME_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol', 'wss://relay.damus.io', 'wss://relay.primal.net']; const sick = new Map(); // normalized url -> { until, n } try { const saved = JSON.parse(localStorage.getItem(SICK_KEY) || '{}'); @@ -125,12 +126,22 @@ try { const saveSick = () => { try { localStorage.setItem(SICK_KEY, JSON.stringify(Object.fromEntries([...sick].slice(-200)))); } catch {} }; +// The relays the app stands on are never written off for long: a phone +// asleep or between networks fails every socket at once, and an escalating +// verdict on relay.coinos.io then kept a profile at the 2 posts primal still +// held for hours after the network was back (2026-10-01). +const HOME_SICK_MS = 60_000; +const isHomeRelay = (n) => HOME_RELAYS.some((u) => relayKey(u) === n); +// Failures while the tab is hidden or offline say nothing about the relay. +const offline = () => typeof navigator !== 'undefined' && navigator.onLine === false; +const hiddenTab = () => typeof document !== 'undefined' && document.visibilityState === 'hidden'; export function markRelaySick(url, why) { if (!url) return; const n = relayKey(url); + if (offline() || hiddenTab()) { resetRelay(url); return; } const prev = sick.get(n); const strikes = prev ? (prev.n || 1) + 1 : 1; - const ms = Math.min(SICK_MAX_MS, SICK_MS * 2 ** (strikes - 1)); + const ms = isHomeRelay(n) ? HOME_SICK_MS : Math.min(SICK_MAX_MS, SICK_MS * 2 ** (strikes - 1)); if (!prev) dlog(`nostr: ${url} unreachable (${why || 'connect failed'}) — skipping for ${Math.round(ms / 60_000)} min`); sick.set(n, { until: Date.now() + ms, n: strikes }); saveSick(); @@ -139,6 +150,18 @@ export function markRelaySick(url, why) { resetRelay(url); } const isSick = (u) => sick.has(relayKey(u)); +// Back online or back on screen: the home relays get a fresh chance at once +// (verdicts saved by older builds could still be hours long). +function forgiveHomeRelays() { + let changed = false; + for (const u of [...sick.keys()]) if (isHomeRelay(u)) { sick.delete(u); changed = true; } + if (changed) saveSick(); +} +forgiveHomeRelays(); +if (typeof window !== 'undefined') { + window.addEventListener('online', forgiveHomeRelays); + document.addEventListener('visibilitychange', () => { if (!hiddenTab()) forgiveHomeRelays(); }); +} // Drop known-bad relays, but never hand back an empty set: if everything is // sick the network probably is, and one doomed attempt beats doing nothing. function liveRelays(list) { @@ -301,7 +324,7 @@ export function nsecOf(sk) { try { return nsecEncode(sk); } catch { return null; // against a real 862-follow list: nos.lol carries 267 of them, damus.io 249, // primal 170 — and damus wasn't here, which left 232 follows with no relay // list of their own leaning on two relays instead of three. -export const PROFILE_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol', 'wss://relay.damus.io', 'wss://relay.primal.net']; +export const PROFILE_RELAYS = HOME_RELAYS; // Fetch a recipient's profile (name + picture) for a pubkey, newest across relays. export async function fetchNostrProfile(pubkeyHex, relays = PROFILE_RELAYS) { let events; |
