A feed's page follows its row after a check (#316)
The header was drawn once, on opening the feed. feed_row put the new row in S.feeds and redrew the feed list but not the open page, so a check that cleared an error left the error showing, with the old "checked" time, until the feed was opened again. Seen on Eli Bendersky's website: a connect timeout at 15:13, a clean forced check at 15:28, and the timeout still on the page. Only the stat line and the error banner are redrawn: the whole page would close the open item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1906,6 +1906,17 @@ test('a refresh button turns from the press to the end of the scan it asked for'
|
||||
expect(await spin('#scanAll .i')).toBe('none');
|
||||
});
|
||||
|
||||
test("an open feed's page follows its row: an error a later check clears goes", async ({ page }) => {
|
||||
// The header was drawn once, on opening, and kept a cleared error until reopened (#316).
|
||||
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
||||
const head = page.locator('.fhead .meta');
|
||||
await page.evaluate(() => patchFeed({ ...S.feeds.find(f => f.id === 'test-show'), last_error: 'connecting: timed out', failing: null }));
|
||||
await expect(head.locator('.fail')).toHaveText('connecting: timed out');
|
||||
await page.evaluate(() => patchFeed({ ...S.feeds.find(f => f.id === 'test-show'), last_error: null, entries: 99 }));
|
||||
await expect(head.locator('.fail')).toHaveCount(0);
|
||||
await expect(head.locator('.stat')).toContainText('99 items');
|
||||
});
|
||||
|
||||
test.describe('with motion reduced', () => {
|
||||
test.use({ reducedMotion: 'reduce' });
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Scrolling to the end of a dialog or the phone's feed list no longer scrolls the page behind it (#312).
|
||||
- Cover artwork has a faint edge, so a dark cover stands off a dark theme (#313).
|
||||
- The tab bar's corners follow the tabs inside it (#314), and the Directory's covers fan out smoothly (#315).
|
||||
- A feed's page drops an error once a later check succeeds, and its item count and when it was last checked stay current, instead of keeping what they were when you opened it (#316).
|
||||
- Settings shows iPX's and the page's versions at its foot: the logo's tooltip was out of reach of a keyboard and of a phone (#309).
|
||||
|
||||
## [1.2.0] - 2026-10-19
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
/* ---------------- feed page ---------------- */
|
||||
function statHTML(f: Feed){
|
||||
return `<div class="sub stat" title="Checked every ${everyText(f.every_mins)}${f.next_check?`, next ${due(f.next_check)}`:''}">${
|
||||
plural(f.entries,'item')}, ${f.downloaded} downloaded · checked ${ago(f.last_checked)}${
|
||||
f.subscribers>1?` · shared with ${f.subscribers-1} other ${f.subscribers===2?'person':'people'}`:''}</div>`;
|
||||
}
|
||||
/// The open feed's counts, last check and error, from a row that came after its page was drawn:
|
||||
/// the page kept an error a later check had cleared (#316). Not the whole page, which would close
|
||||
/// the open item.
|
||||
function patchFeedHead(f: Feed){
|
||||
const meta=document.querySelector<HTMLElement>('#content .fhead .meta'); if(!meta) return;
|
||||
meta.querySelector('.fail')?.remove();
|
||||
const stat=meta.querySelector('.stat'); if(!stat) return;
|
||||
// An OPML's header has a stat line of its own.
|
||||
if(!S.feeds.some(c=>c.group===f.id)) stat.outerHTML=statHTML(f);
|
||||
meta.querySelector('.stat')!.insertAdjacentHTML('afterend',failBannerHTML(f));
|
||||
wireFailBanner(meta,f);
|
||||
}
|
||||
function renderFeed(){
|
||||
paintScanning(); // another feed's page may be the one open now
|
||||
const box=$('#content');
|
||||
@@ -20,9 +37,7 @@ function renderFeed(){
|
||||
${artHTML(f.image,name)}
|
||||
<div class="meta">
|
||||
<h2>${esc(name)}</h2>
|
||||
<div class="sub stat" title="Checked every ${everyText(f.every_mins)}${f.next_check?`, next ${due(f.next_check)}`:''}">${
|
||||
plural(f.entries,'item')}, ${f.downloaded} downloaded · checked ${ago(f.last_checked)}${
|
||||
f.subscribers>1?` · shared with ${f.subscribers-1} other ${f.subscribers===2?'person':'people'}`:''}</div>
|
||||
${statHTML(f)}
|
||||
${failBannerHTML(f)}
|
||||
${f.orphaned?`<div class="sub" style="color:var(--warn)">This feed is no longer listed in its
|
||||
OPML subscription. It was kept rather than removed because it has downloaded items.</div>`:''}
|
||||
|
||||
@@ -5,6 +5,7 @@ let rowsQueued=0;
|
||||
function patchFeed(row: Feed){
|
||||
const i=S.feeds.findIndex(f=>f.id===row.id);
|
||||
if(i<0) S.feeds.push(row); else S.feeds[i]=row;
|
||||
if(row.id===S.feed) patchFeedHead(row);
|
||||
if(!rowsQueued) rowsQueued=requestAnimationFrame(()=>{ rowsQueued=0; renderFeeds(); });
|
||||
}
|
||||
|
||||
@@ -200,10 +201,10 @@ function selectFeed(id: string){
|
||||
/// set (it has been failing for a day and is a kind worth naming -- see `explain_failure` in
|
||||
/// src/feed.rs). Anything else still shows the raw error, as before.
|
||||
function failBannerHTML(f: Feed){
|
||||
if(f.failing) return `<div class="sub" style="color:var(--bad)">${esc(f.failing.reason)}
|
||||
if(f.failing) return `<div class="sub fail" style="color:var(--bad)">${esc(f.failing.reason)}
|
||||
<button type="button" class="btn tiny" data-ffail="unsub">Unsubscribe</button>${
|
||||
f.failing.new_url?` <button type="button" class="btn tiny" data-ffail="newurl">Use the new address</button>`:''}</div>`;
|
||||
if(f.last_error) return `<div class="sub" style="color:var(--bad)">${esc(f.last_error)}</div>`;
|
||||
if(f.last_error) return `<div class="sub fail" style="color:var(--bad)">${esc(f.last_error)}</div>`;
|
||||
return '';
|
||||
}
|
||||
function wireFailBanner(box: ParentNode,f: Feed){
|
||||
|
||||
Reference in New Issue
Block a user