aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-13 09:06:24 -0700
committerAdam Soltys <[email protected]>2026-09-13 09:06:24 -0700
commit848c3edd115360739b5680fec45ef6b71b3b9c1c (patch)
tree8e278613b08b8e616c4a90cdf439d60525078ca8 /tools
parent9fb2154236a3f9d7ede5344cfcc4fa56ff3ada0c (diff)
downloadcoinosv3-848c3edd115360739b5680fec45ef6b71b3b9c1c.tar.gz
coinosv3-848c3edd115360739b5680fec45ef6b71b3b9c1c.zip
Bigger faces in the feed.
Measured on a Pixel 9: Amethyst gives a feed avatar 55dp, we gave it 30 — the same circle the inbox and chat rows use, where a face is a label on a line. In a feed the face is the point. Feed rows now get 44px; everywhere else is untouched. The catch, and the reason this isn't a one-line change: the cached thumbnail was 96px square, sized for a 30px circle at 3x. A 44px circle no longer fits inside it, and the fallback is to layer the ORIGINAL over the thumbnail — which for a feed of twenty rows means twenty full-size downloads, one of which in this wallet's own cache was a 5MB JPEG. That is exactly the slow-avatar problem we already fixed once. So the thumbnail grows with the circle: 144px, which covers 44 at 3x with room to spare. Faces cached at the old size are remade once (thumbPx says which size a thumbnail was made for), and the old one keeps painting meanwhile, so nobody watches a grey circle while it happens. The per-face cap goes 9000 → 16000 chars and the shared budget 180k → 280k, both scaled to the new size: the busiest picture on hand measures 12827 chars at 144px, and at the old cap it would have been rejected and left fetching its original forever. avatar-thumb-test gains the guard that would have caught all of this: the thumbnail must cover the biggest circle in the app at 3x. It failed at 128px (132 needed) — which is why THUMB_PX is 144 and not 128. Its redirect assertion was also order-dependent — the seed writes the profile into every wallet's cache but only the painted one makes a thumbnail, so it asked the first row and got a coin toss. It takes the best copy now. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'tools')
-rw-r--r--tools/avatar-thumb-test.js34
1 files changed, 32 insertions, 2 deletions
diff --git a/tools/avatar-thumb-test.js b/tools/avatar-thumb-test.js
index b532982..727c49a 100644
--- a/tools/avatar-thumb-test.js
+++ b/tools/avatar-thumb-test.js
@@ -109,7 +109,32 @@ try {
});
check('a thumbnail was made and persisted', row.hasThumb, row.kind + ' ' + row.len + ' chars');
check('it is pinned to the picture it came from', row.thumbFor);
- check('it is small', row.len > 0 && row.len < 12000, row.len + ' chars');
+ check('it is small', row.len > 0 && row.len < 16000, row.len + ' chars');
+
+ // The whole point of the thumbnail is that no circle in the app ever has
+ // to fetch the original again. The feed's face is the biggest of them, so
+ // the thumbnail has to cover IT at phone pixel density — if someone grows
+ // that circle without growing THUMB_PX, every feed row quietly starts
+ // pulling down full-size originals again.
+ const covers = await page.evaluate(() => {
+ const el = document.createElement('div');
+ el.className = 'chat-avatar note-avatar';
+ el.style.position = 'absolute'; el.style.visibility = 'hidden';
+ document.body.appendChild(el);
+ const css = parseFloat(getComputedStyle(el).width);
+ el.remove();
+ const rows = Object.keys(localStorage).filter((k) => /^btc-wallet-cache:[0-9a-f]+:profiles$/.test(k))
+ .flatMap((k) => Object.values(JSON.parse(localStorage.getItem(k) || '{}')));
+ const r = rows.find((x) => x && (x.thumb || '').length) || {};
+ return new Promise((done) => {
+ const i = new Image();
+ i.onload = () => done({ css, thumb: i.naturalWidth });
+ i.onerror = () => done({ css, thumb: 0 });
+ i.src = r.thumb || '';
+ });
+ });
+ check('the thumbnail covers the biggest circle at 3x',
+ covers.thumb >= covers.css * 3, `${covers.thumb}px thumbnail for a ${covers.css}px circle`);
// the small avatar paints from the thumbnail alone
const style = await page.evaluate(() => {
@@ -187,7 +212,12 @@ try {
const redirThumb = await page.evaluate(() => {
const rows = Object.keys(localStorage).filter((k) => /^btc-wallet-cache:[0-9a-f]+:profiles$/.test(k))
.flatMap((k) => Object.values(JSON.parse(localStorage.getItem(k) || '{}')));
- const r = rows.find((x) => x && /redir/.test(x.picture || '')) || {};
+ // The seed above wrote this profile into EVERY wallet's cache, but only
+ // the one actually on screen paints the avatar and so makes the
+ // thumbnail. Asking the first matching row was a coin toss on
+ // localStorage key order — take the best copy, not an arbitrary one.
+ const all = rows.filter((x) => x && /redir/.test(x.picture || ''));
+ const r = all.find((x) => (x.thumb || '').length) || all[0] || {};
return { thumb: (r.thumb || '').length, fail: !!r.thumbFail };
});
check('an old thumbnail failure retries immediately and clears on success', redirThumb.thumb > 0 && !redirThumb.fail,