aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-27 00:02:33 -0700
committerAdam Soltys <[email protected]>2026-09-27 00:02:33 -0700
commit1500c642e222208d3c17b13f17520478abd68234 (patch)
treeee6bdb1199edb9f2675806975577d425f30c65b6
parent3395ccc44a7455a4b9de73e78ab655b38b7a827d (diff)
downloadcoinosv3-1500c642e222208d3c17b13f17520478abd68234.tar.gz
coinosv3-1500c642e222208d3c17b13f17520478abd68234.zip
Feed: the foot says when older posts are loading, and when there are none left.
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
-rw-r--r--src/features/messages.js14
-rw-r--r--src/i18n.js2
-rw-r--r--tools/feed-window-dom-test.js19
3 files changed, 29 insertions, 6 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 2b0af4a..dc3c4ee 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -4600,6 +4600,7 @@ export function messagesFeature(ctx) {
const c = feedNow();
if (c.booting || c.loadingMore || (c.shown >= c.notes.length && (c.end || c.status !== 'ready'))) return;
c.loadingMore = true;
+ if (c === feed) render(); // the footer says so while it happens
try {
if (c.shown < c.notes.length) {
const end = Math.min(c.shown + FEED_PAGE, c.notes.length);
@@ -6994,9 +6995,16 @@ export function messagesFeature(ctx) {
: !visible.length
? h('div', { class: 'small faint', style: 'text-align:center;padding:12px 0' }, t('feedEmpty'))
: h('div', { class: 'card col notes-feed', style: 'gap:0', 'data-booting': c.booting ? '1' : undefined }, ...rows),
- // Older posts prepare offscreen; a background fetch should not add
- // a spinner to an already readable feed.
-
+ // The foot of the feed says what is happening down there: older
+ // posts on their way, or nothing older left to fetch. (Older pages
+ // prepare offscreen, so the line is mostly seen by a reader who
+ // outruns them.)
+ c.loadingMore
+ ? h('div', { class: 'row gap6 feed-foot', style: 'justify-content:center;align-items:center;padding:10px 0' },
+ h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('feedLoadingMore')))
+ : c.end && visible.length && !(c.win && c.win.bottom) && c.shown >= visible.length
+ ? h('div', { class: 'small faint feed-foot', style: 'text-align:center;padding:10px 0' }, t('feedEnd'))
+ : null,
...noteOverlays());
}
diff --git a/src/i18n.js b/src/i18n.js
index 3e20a98..bab913d 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -327,6 +327,8 @@ const STR = {
feedNoFollows: "You don't follow anyone yet. Their posts will show up here.",
feedFindPeople: 'Find people',
feedEmpty: 'Nothing posted yet.',
+ feedLoadingMore: 'Loading more posts…',
+ feedEnd: 'You’re all caught up.',
feedAttach: 'Attach a picture or video',
postLike: 'Like',
postBoost: 'Boost',
diff --git a/tools/feed-window-dom-test.js b/tools/feed-window-dom-test.js
index d7a2f8c..7f89785 100644
--- a/tools/feed-window-dom-test.js
+++ b/tools/feed-window-dom-test.js
@@ -41,10 +41,11 @@ await page.evaluateOnNewDocument((all) => {
const f = m[2] || {};
const evs = (f.kinds || []).includes(1) && !f['#e'] && !f.ids
? all.filter((e) => (!f.until || e.created_at <= f.until) && (!f.since || e.created_at >= f.since)).slice(0, f.limit || 100) : [];
+ // an older page takes a relay a moment: long enough for the footer to be seen
setTimeout(() => {
for (const e of evs) ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', m[1], e]) }));
ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) }));
- }, 0);
+ }, f.until ? 900 : 0);
return;
}
if (m[0] === 'EVENT') return;
@@ -95,13 +96,25 @@ try {
console.log('\n[a long scroll]');
check('the feed opens on its first posts', (await dom()).first === 'post number 1', JSON.stringify(await dom()));
// page to the bottom, again and again
- let d = null;
+ let d = null, sawLoading = false;
+ const footNow = () => page.evaluate(() => document.querySelector('.feed-foot')?.textContent || '');
for (let i = 0; i < 14; i++) {
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
- await sleep(700);
+ for (let k = 0; k < 5; k++) { if (/Loading more/.test(await footNow())) sawLoading = true; await sleep(140); }
d = await dom();
}
check('many pages in, the page is long', d.height > 6000, d.height + 'px');
+ // paging to the very end: the footer says loading while it happens, and that there is no more once it is over
+ let foot = '';
+ for (let i = 0; i < 40; i++) {
+ await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
+ foot = await footNow();
+ if (/Loading more/.test(foot)) sawLoading = true;
+ if (/caught up/.test(foot)) break;
+ await sleep(300);
+ }
+ check('the foot of the feed said more was loading', sawLoading);
+ check('...and that you are caught up once there is no more', /caught up/.test(foot), foot);
check('...but only a window of rows is mounted', d.rows > 0 && d.rows <= 60, d.rows + ' rows');
check('...with a spacer standing in for the rows above', d.top > 1000, d.top + 'px');