From 697e907c8630709cbb6df3a9fc75f2a9610fd197 Mon Sep 17 00:00:00 2001 From: rays Date: Mon, 5 Oct 2026 17:37:35 +0000 Subject: [PATCH] Keep the feed list's icons when it redraws, rather than load each again (#134) Checking every feed made all the feeds' icons in the feed list flash while it ran. A check sends each feed's new row as it reads the feed, and renderFeeds, run once a frame while they come in, emptied the list and built every row anew, every with it: each icon went blank and was loaded and drawn again, many times a second through a scan of 150 feeds. The rows are still rebuilt, but the images already drawn go into the new ones in place of the fresh copies, matched by their markup, so only an icon that has changed is made anew. A browser test checks the rows are new and the images in them the same elements, and fails without this. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ tests/ui/app.spec.js | 18 ++++++++++++++++++ web/src/feeds.ts | 6 ++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f2d476..6bee9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Checking every feed no longer makes every icon in the feed list flash while it runs: the list keeps the icons it has already drawn. + ## [0.10.0] - 2026-10-05 ### Added diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index da319d1..8b711bb 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -966,6 +966,24 @@ test('the Directory lists what everyone here reads, the most subscribed first, b await ctx.close(); }); +test('the feed list keeps the icons it has drawn when a feed\'s new row comes in (#134)', async ({ page }) => { + await expect(page.locator('#feedlist .feed').first()).toBeVisible({ timeout: 20_000 }); + const kept = await page.evaluate(async () => { + const frames = () => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); + // Artwork that loads, on a feed of its own row: the fixtures' fails on purpose, for initials. + const row = { ...S.feeds.find(f => !f.group && !S.feeds.some(c => c.group === f.id)), image: '/favicon.png' }; + patchFeed(row); await frames(); + const rows = [...document.querySelectorAll('#feedlist .feed')]; + const before = [...document.querySelectorAll('#feedlist .feed img')]; + // The same feed's new row again, as a check of every feed sends one for each feed it reads. + patchFeed({ ...row }); await frames(); + const after = [...document.querySelectorAll('#feedlist .feed img')]; + return { drawn: before.length > 0, redrawn: !rows[0].isConnected, same: after.length === before.length && after.every((img, i) => img === before[i]) }; + }); + // The rows are new, the images in them the ones already drawn. + expect(kept).toEqual({ drawn: true, redrawn: true, same: true }); +}); + test('adding a feed scans it straight away', async ({ page }) => { await page.locator('#addFeed').click(); await page.locator('#nurl').fill('http://127.0.0.1:8792/fresh.xml'); diff --git a/web/src/feeds.ts b/web/src/feeds.ts index b2471a2..9145180 100644 --- a/web/src/feeds.ts +++ b/web/src/feeds.ts @@ -66,6 +66,11 @@ function renderFeeds(){ const row=back&&list.querySelector(`[data-id="${CSS.escape(back[0])}"]`); if(row) (back[1]&&$('.chev',row)||row).focus(); }; + // The images already drawn, to go into the rows that replace theirs. Made anew, every icon in + // the list went blank and loaded again, once a frame through a check of every feed, as each + // feed's new row came in (#134). Matched by their markup, so a changed one is made anew. + const drawn=new Map(); + for(const img of $$('img',list)){ const k=img.outerHTML; if(!drawn.has(k)) drawn.set(k,[]); drawn.get(k).push(img); } list.innerHTML=''; const unreadAll=S.feeds.reduce((n,f)=>n+(f.unread||0),0); const places=document.createElement('div'); @@ -139,6 +144,7 @@ function renderFeeds(){ `${unread>999?'999+':unread}`; el.onclick=()=>{ selectFeed(f.id); nav(false); }; if(kids) $('.chev',el).onclick=ev=>{ ev.stopPropagation(); toggleGroup(f.id); }; + for(const img of $$('img',el)){ const was=drawn.get(img.outerHTML)?.shift(); if(was) img.replaceWith(was); } list.appendChild(el); } paintScanning();