diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e1cb2..cdbcc54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The unread counts catch up when you come back to the page: an item read on another device, or while the phone app was in the background, no longer stays counted as unread until a reload. - A feed set to download 0 of its newest items downloads all of them, the whole back catalogue, as the server's 0 does; it downloaded nothing. - The feed list loads faster: it counts the items and files of your own feeds, not of every feed on the server. - Downloading or deleting a file the server no longer has answers "not found" instead of a server error. diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index ff57559..0d7dc87 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1683,3 +1683,21 @@ test.describe('on a narrow phone', () => { await page.evaluate(() => setTheme('modern', 'dark')); }); }); + +test('a page brought back into view catches up with what was read elsewhere', async ({ page }) => { + // Reading an item tells no other page, and a phone's page misses everything while it is in the + // background: All Subscriptions went on counting an item read elsewhere as unread (#168). + const badge = page.locator('.place[data-id=":all"] .badge'); + const [one] = (await (await page.request.get('/api/entries?limit=1')).json()).entries; + const flag = read => page.request.post(`/api/entries/${encodeURIComponent(one.feed_id)}/${encodeURIComponent(one.guid)}/flags`, { data: { read } }); + await flag(false); + await page.reload(); + const before = Number(await badge.textContent()); + expect(before).toBeGreaterThan(0); + await flag(true); // read on another device + await page.evaluate(() => { + Object.defineProperty(document, 'hidden', { configurable: true, get: () => false }); + document.dispatchEvent(new Event('visibilitychange')); + }); + await expect(badge).toHaveText(String(before - 1)); +}); diff --git a/web/src/events.ts b/web/src/events.ts index 49a9cad..06d8c49 100644 --- a/web/src/events.ts +++ b/web/src/events.ts @@ -1,7 +1,19 @@ /* ---------------- live events ---------------- */ let sse; +/// The feed list and the open list, taken again from the server. The events keep both current, +/// and one missed is a count wrong until a reload: reading an item sends none to your other +/// pages, so one read on another device stayed unread here, and an iPhone stops the stream while +/// the app is in the background, when everything is missed (#168). +const catchUp=(()=>{ let t; return ()=>{ clearTimeout(t); t=setTimeout(()=>{ + loadFeeds(true).catch(()=>{}); + if(S.feed) loadEntries().catch(()=>{}); +},300); }; })(); +document.addEventListener('visibilitychange',()=>{ if(!document.hidden) catchUp(); }); +let dropped=false; function connect(){ sse=new EventSource('/api/events'); + // Back after a drop: whatever was sent meanwhile went to nobody. + sse.onopen=()=>{ if(dropped){ dropped=false; catchUp(); } }; const soon=(fn,ms=500)=>{ let t; return ()=>{ clearTimeout(t); t=setTimeout(fn,ms); }; }; const refreshEntries=soon(()=>{ if(S.feed) loadEntries(); }); // Every scan's events reach everyone; only this person's feeds are theirs to show or refresh. @@ -48,7 +60,7 @@ function connect(){ // page reloads; reload the list here when a scan synced an OPML if that ever matters. else if(ev.ev==='scan_done'){ scanning.clear(); paintScanning(); if(ev.feeds) refreshEntries(); } }; - sse.onerror=()=>{ sse.close(); setTimeout(connect,4000); }; + sse.onerror=()=>{ sse.close(); dropped=true; setTimeout(connect,4000); }; } connect(); loadFeeds();