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:
@@ -262,3 +262,14 @@ test('a tap ended over a minute after its last request reports once, without rec
|
|||||||
run('endTap()');
|
run('endTap()');
|
||||||
assert.equal(run(`sent.filter(([u, o]) => JSON.parse(o.body).ev === 'tap').length`), 1);
|
assert.equal(run(`sent.filter(([u, o]) => JSON.parse(o.body).ev === 'tap').length`), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the next page starts after the unread ones on the Unread tab, after all of them elsewhere', () => {
|
||||||
|
run(`S.entries = [{guid:'a',read:true},{guid:'b',read:false},{guid:'c',read:true}]; S.total = 3`);
|
||||||
|
run(`S.filter = 'unread'`);
|
||||||
|
// Two read here are off the server's unread list: starting at 3 would skip two unread ones (#383).
|
||||||
|
assert.equal(run('nextOffset()'), 1);
|
||||||
|
assert.equal(run('hasMore()'), true);
|
||||||
|
run(`S.filter = 'all'`);
|
||||||
|
assert.equal(run('nextOffset()'), 3);
|
||||||
|
assert.equal(run('hasMore()'), false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
### 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).
|
- 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).
|
- 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).
|
- Switching away from the page a minute after using it no longer stops it with "Maximum call stack size exceeded" (#378).
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async function loadEntries(append?: boolean, quiet?: boolean){
|
|||||||
else if(!append&&!quiet) spin=setTimeout(()=>{ if(seq===loadSeq) listLoading(); },120);
|
else if(!append&&!quiet) spin=setTimeout(()=>{ if(seq===loadSeq) listLoading(); },120);
|
||||||
let r: ListPage;
|
let r: ListPage;
|
||||||
try{
|
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); }
|
}finally{ clearTimeout(spin); }
|
||||||
// Only the latest list counts: marking everything read reloads the All tab, and clicking
|
// 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.
|
// 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));
|
const w=readWrites.get(entryKey(e));
|
||||||
if(w && !(w.done!==undefined&&w.done<asked)) e.read=w.read;
|
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
|
// 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.
|
// 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.
|
// 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));
|
for(const e of S.entries) box.appendChild(epEl(e));
|
||||||
syncPlayButtons();
|
syncPlayButtons();
|
||||||
if(pane) pane.scrollTop=top;
|
if(pane) pane.scrollTop=top;
|
||||||
if(S.entries.length < S.total){
|
if(hasMore()){
|
||||||
const b=document.createElement('button');
|
const b=document.createElement('button');
|
||||||
b.className='btn'; b.id='more'; b.textContent=`Load more (${S.entries.length} of ${S.total})`;
|
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);
|
box.appendChild(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +319,21 @@ function selectEntry(e: Entry,turning=false){
|
|||||||
swapRow(e);
|
swapRow(e);
|
||||||
showDetail(e);
|
showDetail(e);
|
||||||
const d=$('#detail'); if(d) d.scrollTop=0;
|
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.
|
/// Drag the divider between the item list and the item text.
|
||||||
|
|||||||
Reference in New Issue
Block a user