diff options
| author | Adam Soltys <[email protected]> | 2026-09-11 22:55:15 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-11 22:55:15 -0700 |
| commit | c4677d7c5d66afe6ac5c7eb277cf8c3b2e08e62a (patch) | |
| tree | 0b6a7523f36fafe7d227dec48e18727ff1ad9937 | |
| parent | e9a0aa2ca4d3aa659b579295dd983decd3729b6f (diff) | |
| download | coinosv3-c4677d7c5d66afe6ac5c7eb277cf8c3b2e08e62a.tar.gz coinosv3-c4677d7c5d66afe6ac5c7eb277cf8c3b2e08e62a.zip | |
The feed opens at twenty posts and runs to the page's edge.
It was rendering everything it held — a hundred-odd notes to show the few you
read. It now paints twenty, and reaching the bottom shows twenty more; only
when the window catches up with what's cached does it go back to the relays
for older posts. Posts arriving live at the top widen the window rather than
pushing off the one you'd scrolled to.
And the posts are the page now, not a card sitting inside one: they step out
through the chat page's padding to the container's own edge, leaving one
continuous list with hairlines between the posts instead of two sets of card
walls.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
| -rw-r--r-- | src/features/messages.js | 19 | ||||
| -rw-r--r-- | src/style.css | 8 | ||||
| -rw-r--r-- | tools/feed-follow-test.js | 21 |
3 files changed, 45 insertions, 3 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 6020951..24c9006 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2453,6 +2453,7 @@ export function messagesFeature(ctx) { // rather than a spinner, exactly like the profile pages do. const FEED_CACHE = 'feedNotes'; const FEED_LIMIT = 80; + const FEED_PAGE = 20; // posts on screen at once, grown as you scroll const FEED_KEEP = 200; // in memory const FEED_STORE = 50; // ...and on disk let feed = null; // { status, notes, end, loadingMore } @@ -2466,7 +2467,7 @@ export function messagesFeature(ctx) { if (!feed) { let stored = []; try { stored = wallet.loadFeatureState(FEED_CACHE, []) || []; } catch {} - feed = { status: stored.length ? 'ready' : 'loading', notes: stored }; + feed = { status: stored.length ? 'ready' : 'loading', notes: stored, shown: FEED_PAGE }; refreshFeed(); } return feed; @@ -2483,6 +2484,9 @@ export function messagesFeature(ctx) { const add = (evs || []).filter((e) => e.kind === 1 && !isReply(e) && !seen.has(e.id) && seen.add(e.id)); if (!add.length) return false; c.notes = [...c.notes, ...add].sort((a, b) => b.created_at - a.created_at).slice(0, FEED_KEEP); + // posts arriving at the TOP shouldn't cost you the ones you'd scrolled to + const fresh = add.filter((e) => e.created_at >= (c.notes[0] || {}).created_at).length; + if (fresh) c.shown = Math.min((c.shown || FEED_PAGE) + fresh, c.notes.length); try { wallet.saveFeatureState(FEED_CACHE, c.notes.slice(0, FEED_STORE).map(slimNote)); } catch {} return true; } @@ -2525,15 +2529,24 @@ export function messagesFeature(ctx) { for (const u of feedUnsubs) { try { u(); } catch {} } feedUnsubs = []; } + // Reaching the bottom shows another twenty. Only when the window has caught + // up with everything we hold do we go back to the relays for older posts — + // rendering a hundred notes to show twenty was the whole cost here. async function loadOlderFeed() { const c = feedNow(); + if (c.shown < c.notes.length) { + c.shown = Math.min(c.shown + FEED_PAGE, c.notes.length); + render(); + if (c.shown < c.notes.length) return; // still serving from what we have + } if (c.status !== 'ready' || c.loadingMore || c.end) return; const oldest = c.notes[c.notes.length - 1]; if (!oldest || !feedAuthors().length) { c.end = true; return; } c.loadingMore = true; render(); try { - if (!await feedPass({ until: oldest.created_at - 1 })) c.end = true; + if (await feedPass({ until: oldest.created_at - 1 })) c.shown += FEED_PAGE; + else c.end = true; } catch {} finally { c.loadingMore = false; render(); @@ -3356,7 +3369,7 @@ export function messagesFeature(ctx) { function feedView() { const c = feedNow(); const authors = feedAuthors(); - const rows = c.notes.flatMap((ev, i) => [ + const rows = c.notes.slice(0, c.shown || FEED_PAGE).flatMap((ev, i) => [ i ? h('div', { style: 'height:1px;background:var(--border,rgba(128,128,128,.18));margin:0 -14px' }) : null, noteRow(ev.pubkey, ev, displayName(ev.pubkey)), ]); diff --git a/src/style.css b/src/style.css index a83e4df..13198cc 100644 --- a/src/style.css +++ b/src/style.css @@ -542,6 +542,14 @@ hr.divider { margin: 16px 0; } border-left: none; border-right: none; border-radius: 0; } } +/* Inside the chat page the posts ARE the page, not a card sitting in one: + they step out through its padding to the container's own edge and drop the + inner walls, leaving one continuous list with a hairline top and bottom. */ +.chat-page .notes-feed { + margin-left: -20px; margin-right: -20px; + border-left: none; border-right: none; border-radius: 0; + box-shadow: none; +} /* Inline move-money panel on the balance card. */ diff --git a/tools/feed-follow-test.js b/tools/feed-follow-test.js index 35fab2d..4c1fed3 100644 --- a/tools/feed-follow-test.js +++ b/tools/feed-follow-test.js @@ -99,6 +99,27 @@ try { dialled.filter((u) => u.includes('example.invalid')).join(' ') || 'neither dialled'); check('...and our own relays are still asked as well', dialled.some((u) => u.includes('relay.coinos.io')), dialled.length + ' sockets'); + + // The feed opens at twenty posts and grows as you reach the bottom, however + // many it is holding. + await page.evaluate((k) => { + const notes = []; + const now = Math.floor(Date.now() / 1000); + for (let i = 0; i < 60; i++) notes.push({ id: String(i).padStart(64, '0'), pubkey: 'f'.repeat(64), kind: 1, created_at: now - i * 60, content: 'post ' + i, tags: [] }); + localStorage.setItem(k + ':feedNotes', JSON.stringify(notes)); + }, base); + await page.reload({ waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); + await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => /message/i.test(e.getAttribute('aria-label') || '')); if (b) b.click(); }); + await sleep(1000); + await clickItem('feed'); + await sleep(1500); + const first = await page.evaluate(() => document.querySelectorAll('.notes-feed > .row').length); + check('the feed opens at twenty posts, not sixty', first === 20, first + ' rows'); + await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight)); + await sleep(1200); + const second = await page.evaluate(() => document.querySelectorAll('.notes-feed > .row').length); + check('reaching the bottom shows twenty more', second === 40, second + ' rows'); } finally { await browser.close(); server.stop(true); } console.log(ok ? '\n✅ follows and feed' : '\n❌ failed'); process.exit(ok ? 0 : 1); |
