Swiping past the 50th item fetches the next ones (#383)

The item list is fetched 50 at a time, and only the list's Load more
button asked for more. A swipe or j stops at the last item loaded, so
after 50 items the reader bounced on every swipe until a reload brought
a fresh page of unread ones.

Turning to within five of the end now fetches the next page behind. Its
offset counts what is still unread on the Unread tab: the items read
here have left the server's unread list, and Load more's flat 50 a page
skipped as many unread items as had been read. A refresh (a return from
the background, a live update) always asks for the first page, as the
kept list it fills is one, where it used to ask for whatever page Load
more had last reached. A page appended after new items arrived no longer
repeats ones already loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 20:36:45 +00:00
parent c8cbb3047a
commit 385589ff77
3 changed files with 32 additions and 4 deletions

View File

@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Swiping, or j, through more than 50 items no longer stops at the 50th until a reload: the next ones are fetched as you near the end, and Load more on the Unread tab no longer skips as many unread items as you had read (#383).
- ipx-web no longer logs a deprecation warning about its metrics setting at every start (#381).
- A tap's time in the traces ends with the last answer to what it set off, not with a refresh made seconds later (#379).
- Switching away from the page a minute after using it no longer stops it with "Maximum call stack size exceeded" (#378).

View File

@@ -90,7 +90,7 @@ async function loadEntries(append?: boolean, quiet?: boolean){
else if(!append&&!quiet) spin=setTimeout(()=>{ if(seq===loadSeq) listLoading(); },120);
let r: ListPage;
try{
r=await (!append&&!kept&&fetching.get(key) || api(listUrl(feed,S.filter,S.offset)));
r=await (!append&&!kept&&fetching.get(key) || api(listUrl(feed,S.filter,append?S.offset:0)));
}finally{ clearTimeout(spin); }
// Only the latest list counts: marking everything read reloads the All tab, and clicking
// Unread straight after could have that answer land last and replace the Unread one.
@@ -108,7 +108,8 @@ function showList(r: ListPage, append: boolean, asked: number){
const w=readWrites.get(entryKey(e));
if(w && !(w.done!==undefined&&w.done<asked)) e.read=w.read;
}
let entries = append ? S.entries.concat(r.entries) : r.entries;
// An item new since the last page shifts the next one down by it, repeating one already here.
let entries = append ? S.entries.concat(r.entries.filter(e=>!S.entries.some(x=>entryKey(x)===entryKey(e)))) : r.entries;
// A background scan finishing refreshes the list from the server, which -- on the Unread
// tab -- would drop the item you have open the moment reading it took it off the filter.
// Keep it until you pick a different one; the next refresh after that no longer protects it.
@@ -147,10 +148,10 @@ function renderEntries(){
for(const e of S.entries) box.appendChild(epEl(e));
syncPlayButtons();
if(pane) pane.scrollTop=top;
if(S.entries.length < S.total){
if(hasMore()){
const b=document.createElement('button');
b.className='btn'; b.id='more'; b.textContent=`Load more (${S.entries.length} of ${S.total})`;
b.onclick=()=>{S.offset+=LIMIT;loadEntries(true)};
b.onclick=()=>{S.offset=nextOffset();loadEntries(true)};
box.appendChild(b);
}
}
@@ -318,6 +319,21 @@ function selectEntry(e: Entry,turning=false){
swapRow(e);
showDetail(e);
const d=$('#detail'); if(d) d.scrollTop=0;
if(turning) loadAhead();
}
/// Where the next page starts. On the Unread tab the items read here are no longer unread on the
/// server, so counting them, as a flat 50 a page did, skipped as many unread ones.
function nextOffset(){ return S.filter==='unread' ? S.entries.filter(e=>!e.read).length : S.entries.length; }
const hasMore=()=>nextOffset()<S.total;
/// Turning near the end of what is loaded fetches the next page behind: only the list's Load more
/// button did, and a swipe or j stopped dead at the 50th item until a reload (#383).
let loadingAhead=false;
function loadAhead(){
const i=S.entries.findIndex(x=>entryKey(x)===S.sel);
if(loadingAhead || S.entries.length-1-i>5 || !hasMore()) return;
loadingAhead=true; S.offset=nextOffset();
loadEntries(true).catch(()=>{}).finally(()=>{ loadingAhead=false; });
}
/// Drag the divider between the item list and the item text.