diff options
| -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); |
