aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-11 22:55:15 -0700
committerAdam Soltys <[email protected]>2026-09-11 22:55:15 -0700
commitc4677d7c5d66afe6ac5c7eb277cf8c3b2e08e62a (patch)
tree0b6a7523f36fafe7d227dec48e18727ff1ad9937
parente9a0aa2ca4d3aa659b579295dd983decd3729b6f (diff)
downloadcoinosv3-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.js19
-rw-r--r--src/style.css8
-rw-r--r--tools/feed-follow-test.js21
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);