From 7c089faafe943e84652cae19ee53e95932d715b9 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 28 Sep 2026 09:42:03 -0700 Subject: Master builds again, take two: the pill commit re-swept the other agent's feed-snapshot hunks. Same cause as aad978d (a shared working tree); same fix: messages.js in master is the last clean version plus this commit's own three changes (the sound button leaving once unmuted, the list action under the profile row). The working tree keeps the in-progress edits for their author. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D --- src/features/messages.js | 58 +++++++++--------------------------------------- 1 file changed, 10 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/features/messages.js b/src/features/messages.js index 277b9b1..f57c3ee 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -15,7 +15,7 @@ import { subscribeOn, publishOn, queryOn, fetchInboxRelays, relayAlive, liveRelayList, resetRelay, npubOf, neventOf, parseNostrPubkey, parseNostrRef, generateSecretKey, getPublicKey, finalizeEvent, nip44, - PROFILE_RELAYS, openWrapsOffthread, unwrapDMsOffthread, verifyEventsAsync, + PROFILE_RELAYS, openWrapsOffthread, unwrapDMsOffthread, } from '../nostr.js'; import { channelKey, channelStream, channelEpoch, channelIsPrivate, controlKey, guestbookKey, openWrap, wrapRumor, rumorWithId, @@ -32,7 +32,6 @@ import { saveInbox } from '../dm-inbox.js'; import { mergeFeedWindow } from '../feed-window.js'; import { createThreadStore } from '../thread-cache.js'; import { createFeedCache, FEED_CACHE_POSTS } from '../feed-cache.js'; -import { PUBLIC_FEED_RELAYS, popularCandidates } from '../popular-feed.js'; import { makeSearcher, resultRows, fallbackAvatar, warmSearch, punkImageUrl, punkSmallUrl } from '../recipient-search.js'; import { getNetwork } from '../api.js'; import { decodeBolt11 } from '../ark/lightning.js'; @@ -3949,7 +3948,7 @@ export function messagesFeature(ctx) { const RELAY_PICKS = ['wss://relay.coinos.io', 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.snort.social', 'wss://nostr.mom', 'wss://nostr.bitcoiner.social', 'wss://eden.nostr.land', 'wss://nostr.land', 'wss://nostr.oxtr.dev', 'wss://relay.nostrplebs.com', 'wss://offchain.pub', 'wss://nostr21.com', 'wss://relay.nostr.net', 'wss://nostr-pub.wellorder.net']; - const FIREHOSE_RELAYS = PUBLIC_FEED_RELAYS; + const FIREHOSE_RELAYS = ['wss://relay.coinos.io', 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net']; const normRelay = (u) => { let x = String(u || '').trim(); if (!x) return null; @@ -3967,38 +3966,10 @@ export function messagesFeature(ctx) { // latest reactions, reposts and zaps, counted per post by distinct // people, then the posts enough people touched. Older pages walk the // reactions back in time. The prose and language tests run at the door. + const POPULAR_MIN = 2; const POPULAR_ASK = 600; // reactions per round const curateFeed = (evs) => { const lang = getLang(); return evs.filter((e) => !isMachinePost(e.content) && inLanguage(e.content, lang)); }; async function popularPass(extra, merge, c, def) { - // Only the default public feed shares a snapshot. Custom relays and older - // pages keep their own queries. A static/offline install falls back quickly. - if (extra.until == null && !feedRelays(def)) { - try { - const response = await fetch('/api/feed', { signal: AbortSignal.timeout(1500) }); - if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) throw new Error('No feed cache'); - const data = await response.json(); - if (data.version !== 1 || !Number.isFinite(data.generatedAt) || Date.now() - data.generatedAt > 86400_000 - || !Array.isArray(data.notes) || data.notes.length > 240 || !Array.isArray(data.profiles) || data.profiles.length > 240 - || !Number.isFinite(data.popUntil)) throw new Error('Invalid feed cache'); - const events = [...data.notes, ...data.profiles]; - // HTTP is another untrusted source: retain the relay signature checks. - const valid = await verifyEventsAsync(events); - if (c.stopped) return false; - const posts = data.notes.filter((e, i) => valid[i] && e.kind === 1); - const usable = curateFeed(posts).filter((e) => !isReply(e) && !hidden(e)); - if (usable.length) { - warmProfiles(); - for (let i = 0; i < data.profiles.length; i++) { - const e = data.profiles[i]; - if (valid[data.notes.length + i] && e.kind === 0) applyProfile(e.pubkey, e); - } - c.popUntil = c.popUntil || data.popUntil; - const got = await mergeFeed(posts, { ...merge, immediate: true }, c); - if (got) scheduleRepaint(); - return got; // already-known posts also count as a successful cache hit - } - } catch {} // relay path also covers snapshots with no posts in this language - } const relays = feedRelaysOr(def, FIREHOSE_RELAYS); const wide = [...new Set([...relays, ...NOTE_RELAYS])]; let until = extra.until != null && c.popUntil ? c.popUntil - 1 : undefined; @@ -4006,15 +3977,19 @@ export function messagesFeature(ctx) { for (let round = 0; round < 3 && !c.stopped; round++) { const re = await queryOn(wide, { kinds: [6, 7, 9735], limit: POPULAR_ASK, ...(until ? { until } : {}) }, 4500).catch(() => []); if (!re.length) break; - const candidates = popularCandidates(re); + const counts = new Map(); + for (const r of re) { + const who = r.kind === 9735 ? ((r.tags.find((x) => x[0] === 'P') || [])[1] || r.pubkey) : r.pubkey; + for (const x of r.tags) if (x[0] === 'e' && /^[0-9a-f]{64}$/.test(x[1] || '')) (counts.get(x[1]) || counts.set(x[1], new Set()).get(x[1])).add(who); + } const oldest = Math.min(...re.map((r) => r.created_at)); c.popUntil = c.popUntil && until ? Math.min(c.popUntil, oldest) : (c.popUntil || oldest); until = oldest - 1; - const ids = candidates.ids; + const ids = [...counts.entries()].filter(([, s]) => s.size >= POPULAR_MIN).sort((a, b) => b[1].size - a[1].size).slice(0, 80).map(([id]) => id); if (!ids.length) continue; const asks = []; for (let i = 0; i < ids.length; i += 40) asks.push(queryOn(wide, { ids: ids.slice(i, i + 40) }, 4500).catch(() => [])); - const posts = (await Promise.all(asks)).flat().filter(candidates.qualifies); + const posts = (await Promise.all(asks)).flat().filter((e) => e && e.kind === 1 && (counts.get(e.id) || new Set()).size - (counts.get(e.id)?.has(e.pubkey) ? 1 : 0) >= POPULAR_MIN); if (c.stopped) return got; if (await mergeFeed(posts, merge, c)) { got = true; scheduleRepaint(); } if (got && round >= 0 && (c.notes.length >= FEED_PAGE || extra.until != null)) break; @@ -4732,19 +4707,6 @@ export function messagesFeature(ctx) { } if (c.booting) await c.boot; if (c.stopped) return false; - if (opts.immediate && !c.notes.length && !opts.catchup) { - // The public snapshot already contains verified posts and profiles. - // Paint text now using the same progressive rows as the local cache; - // slow media/quotes must not hold the whole first page behind a spinner. - c.booting = true; - admitFeed(add, c); - c.boot = notesReady(c.notes.slice(0, FEED_PAGE)).finally(() => { - c.booting = false; - if (!c.stopped && c === feed) scheduleRepaint(); - }); - scheduleRepaint(); - return true; - } await notesReady(add); if (c.stopped) return false; for (const e of add) staged.delete(e.id); -- cgit v1.2.3