aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-12 22:15:10 -0700
committerAdam Soltys <[email protected]>2026-09-12 22:15:10 -0700
commitd57962cb896b360295ec107afd50d2ce8701c401 (patch)
tree4cdce2945263e310cda12197a5eb049fc8e64ad8 /src/features
parent6cbb1b0c3665191ad486dc43d70dfbd264780f19 (diff)
downloadcoinosv3-d57962cb896b360295ec107afd50d2ce8701c401.tar.gz
coinosv3-d57962cb896b360295ec107afd50d2ce8701c401.zip
The feed was showing half the posts it should.
Compared against a wider net over the same six hours and the same follow list — 862 people, read off this wallet: before: 165 notes from 38 people after: 327 notes from 86 people, and nothing the wider net still finds Four things were losing posts, in order of how much: THE AUTHOR CAP. feedAuthors() stopped at 500. On an 862-follow list that silently dropped 362 people, and 164 of the 166 posts the wider net found were theirs. Gone: it takes everyone now, up to a couple of thousand, with each REQ carrying at most 400 authors so no single filter is absurd. THE DEFAULTS. relay.damus.io is the second-biggest relay this follow list writes to — 249 of them — and it wasn't in our set. 232 follows publish no relay list at all and lean entirely on those defaults, so they were being asked for on two relays instead of three. THE SOCKET CAP. Ten relays covered all but 66 follows; twenty-five covered all but 26. It's 24 now — Amethyst runs near a hundred, and this is the same idea kept to a number a phone can hold. MALFORMED RELAY LISTS. One list in these follows crams three URLs into a single r tag, space-separated. It passed our "starts with wss://" check, reached the pool, threw Invalid URL — and every author covered by that relay quietly returned nothing. Each URL has to parse now. And a quote-post is no longer mistaken for a reply. NIP-10 marks a reply's e tags 'root' or 'reply'; a quote's are 'mention', and NIP-18 quotes carry a q tag. We were hiding every note with an e tag, which included the quote-posts that now render as cards. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js53
1 files changed, 44 insertions, 9 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index aa74387..cd601d9 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -2452,7 +2452,10 @@ export function messagesFeature(ctx) {
// sockets that between them reach everyone rather than one socket each.
const RELAY_LISTS = 'relayLists';
const RELAY_LIST_TTL = 7 * 24 * 3600_000;
- const OUTBOX_MAX = 10; // sockets we'll open for one pass
+ // Sockets we'll open for one pass. Ten covered all but 66 of a real
+ // 862-follow list; twenty-five covered all but 26. Amethyst runs near a
+ // hundred — this is the same idea, kept to a number a phone can hold.
+ const OUTBOX_MAX = 24;
const PER_AUTHOR = 3; // write relays taken from any one list
let relayLists = null; // pk -> { r: [url], t }
@@ -2480,8 +2483,17 @@ export function messagesFeature(ctx) {
wallet.saveFeatureState(RELAY_LISTS, out);
} catch {}
}
+ // A relay list is written by whoever owns it, and some of them are wrong:
+ // one in this wallet's follows has three URLs crammed into a single r tag,
+ // space-separated. Fed to the pool that throws, and every author covered by
+ // that relay quietly returns nothing. So each one has to parse as a URL,
+ // and anything else is dropped rather than trusted.
+ const okRelay = (u) => {
+ if (typeof u !== 'string' || /\s/.test(u) || u.length > 200) return false;
+ try { const p2 = new URL(u); return p2.protocol === 'wss:' && !!p2.host; } catch { return false; }
+ };
const writeRelaysIn = (ev) => [...new Set((ev.tags || [])
- .filter((x) => x[0] === 'r' && x[1] && x[2] !== 'read' && /^wss:\/\//i.test(x[1]))
+ .filter((x) => x[0] === 'r' && x[2] !== 'read' && okRelay(x[1]))
.map((x) => String(x[1]).replace(/\/+$/, '')))].slice(0, PER_AUTHOR);
// One REQ per hundred authors, against our own relays — a relay list is
@@ -2561,8 +2573,26 @@ export function messagesFeature(ctx) {
let feedUnsubs = [];
let feedAt = 0;
- const isReply = (ev) => (ev.tags || []).some((x) => x[0] === 'e');
- const feedAuthors = () => [...followsNow().set].slice(0, 500);
+ // A reply, as against a post that merely POINTS at another one. NIP-10
+ // marks a reply's e tags 'root' or 'reply'; a quote's are 'mention', and
+ // NIP-18 quotes carry a q tag instead. Treating every e tag as a reply hid
+ // quote-posts from the feed — the thing Amethyst shows as a card with the
+ // quoted note inside it.
+ const isReply = (ev) => {
+ const es = (ev.tags || []).filter((x) => x[0] === 'e');
+ if (!es.length) return false;
+ if (es.some((x) => x[3] === 'root' || x[3] === 'reply')) return true;
+ if (es.every((x) => x[3] === 'mention')) return false;
+ // unmarked (the deprecated positional form): a q tag means it's a quote,
+ // otherwise assume the older convention, where e meant reply
+ return !(ev.tags || []).some((x) => x[0] === 'q');
+ };
+ // Everyone you follow. This was capped at 500, which on an 862-follow list
+ // meant 362 people were silently missing from the feed — 164 of the 166
+ // posts a wider net found in one six-hour window were theirs.
+ const FEED_AUTHORS_MAX = 2000;
+ const REQ_AUTHORS = 400; // authors per REQ, so one filter stays a sane size
+ const feedAuthors = () => [...followsNow().set].slice(0, FEED_AUTHORS_MAX);
function feedNow() {
if (!feed) {
@@ -2600,9 +2630,13 @@ export function messagesFeature(ctx) {
await fetchRelayLists(authors);
const plan = outboxPlan(authors);
let got = false;
- await Promise.all(plan.map(async ({ relays, authors: a }) => {
- const evs = await queryOn(relays, { kinds: [1], authors: a, limit: FEED_LIMIT, ...extra }, 5000).catch(() => []);
- if (mergeFeed(evs)) { got = true; scheduleRepaint(); }
+ await Promise.all(plan.flatMap(({ relays, authors: a }) => {
+ const chunks = [];
+ for (let i = 0; i < a.length; i += REQ_AUTHORS) chunks.push(a.slice(i, i + REQ_AUTHORS));
+ return chunks.map(async (chunk) => {
+ const evs = await queryOn(relays, { kinds: [1], authors: chunk, limit: FEED_LIMIT, ...extra }, 5000).catch(() => []);
+ if (mergeFeed(evs)) { got = true; scheduleRepaint(); }
+ });
}));
return got;
}
@@ -2623,8 +2657,9 @@ export function messagesFeature(ctx) {
if (!feedAuthors().length || !ui.chatOpen || ui.msgView !== 'feed') return;
const since = Math.floor(Date.now() / 1000) - 60;
for (const { relays, authors } of outboxPlan(feedAuthors()))
- feedUnsubs.push(subscribeOn(relays, { kinds: [1], authors, since },
- (ev) => { if (mergeFeed([ev])) scheduleRepaint(); }));
+ for (let i = 0; i < authors.length; i += REQ_AUTHORS)
+ feedUnsubs.push(subscribeOn(relays, { kinds: [1], authors: authors.slice(i, i + REQ_AUTHORS), since },
+ (ev) => { if (mergeFeed([ev])) scheduleRepaint(); }));
}
function stopFeedWatch() {
for (const u of feedUnsubs) { try { u(); } catch {} }