aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js48
-rw-r--r--src/nostr.js27
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;