aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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);