<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinosv3.git/tools/avatar-thumb-test.js, branch fdroid-v1.7</title>
<subtitle>coinos v3 — bitcoin, Ark, Lightning and nostr wallet (web + Android)</subtitle>
<id>https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.7</id>
<link rel='self' href='https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.7'/>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/'/>
<updated>2026-09-13T16:48:54Z</updated>
<entry>
<title>The front door points at your own keys.</title>
<updated>2026-09-13T16:48:54Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-13T16:48:54Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=38b3d091e8e1bece958c0d7e80a5ee78434de933'/>
<id>urn:sha1:38b3d091e8e1bece958c0d7e80a5ee78434de933</id>
<content type='text'>
Four buttons of equal weight, and only the sign-in ones said what they
did. "Get started" said nothing, so the concrete offer on that screen was
Google — which is a convenience wrapped around a seed phrase, not an
alternative to one.

Now the page says what the wallet is before it offers a button, the way
in reads "Create a new wallet" with the cost under it (a minute, twelve
words), and the three sign-ins sit below a line labelled "or, if you
already have an account".

And the second half of the same problem: a wallet that arrives by sign-in
has a recovery phrase its owner has never seen and nobody can reissue.
So the wizard shows it once, right after the first login, while it still
reads as setting up rather than as a warning out of nowhere. "I've
written it down" is the way on; "Later" is a real answer, and the wallet
then keeps one quiet line — not a modal, not dismissible, gone the moment
the phrase is actually seen, here or in settings.

seedSeen records that per account: creating shows the words, importing
means they typed them, revealing in settings counts. Only a derived seed
starts false, which is exactly the case worth asking about.

tools/onboard-backup-test.js covers both halves, including that "Later"
leaves a reminder that survives a reload and leads somewhere.

The sixteen tests that clicked the old button by its text are updated,
and it has an id now so its copy can change without breaking them again.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Bigger faces in the feed.</title>
<updated>2026-09-13T16:06:24Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-13T16:06:24Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=848c3edd115360739b5680fec45ef6b71b3b9c1c'/>
<id>urn:sha1:848c3edd115360739b5680fec45ef6b71b3b9c1c</id>
<content type='text'>
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) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Retry avatar thumbnail failures from the old download method</title>
<updated>2026-09-11T06:25:46Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-11T06:25:46Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=0e25159698ab0202f53e8b061e8659d7cde5279c'/>
<id>urn:sha1:0e25159698ab0202f53e8b061e8659d7cde5279c</id>
<content type='text'>
</content>
</entry>
<entry>
<title>The thumbnail fetches the bytes instead of trusting a crossOrigin &lt;img&gt;.</title>
<updated>2026-09-11T06:15:15Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-11T06:15:15Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=25c8f34c98f997371616c12f83687561117ea3b5'/>
<id>urn:sha1:25c8f34c98f997371616c12f83687561117ea3b5</id>
<content type='text'>
Pinguimdarelva's avatar still flashed, and the reason was one I'd have kept
guessing at: from the live browser, `new Image()` with crossOrigin set FAILS
for that picture, while `fetch(url, {mode:'cors', cache:'reload'})` for the
very same URL returns 200. The host allows us perfectly well.

The picture sits behind a 302, and the avatar is painted as an ordinary
background-image — so the bytes are already in the HTTP cache under the
redirect's target as a no-CORS entry, and the crossOrigin &lt;img&gt; reuses that
entry and fails the CORS check. The image loads, the CORS copy "errors", and
the face gets written off.

So the thumbnailer fetches the bytes itself and draws from a blob: no
crossOrigin element, no cache collision, and an untainted canvas without
depending on the element's CORS bookkeeping. With no taint left to detect,
failures no longer split into hard and soft — each one just backs off, from
six hours up to a fortnight, so a host that truly won't have us is asked
twice a month instead of every boot.

The test now serves a picture behind a redirect and loads it plainly before
thumbnailing it, which is the shape that broke; it fails on the old code.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>A face we've never seen is drawn from the pubkey, not left blank.</title>
<updated>2026-09-11T06:00:21Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-11T06:00:21Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=48e8e588f1a3dee26a2497d1a1757f2f294413a3'/>
<id>urn:sha1:48e8e588f1a3dee26a2497d1a1757f2f294413a3</id>
<content type='text'>
The avatar delay left on the messages page wasn't the image at all. A person
with no cached profile got an empty circle until a relay answered — and the
one that stayed blank longest (npub1k6f89cg, in the live cache) is never
cached at all: persistProfile deliberately refuses to write a row with no
name and no picture, so every boot re-asked the relays and every boot sat
blank for seconds. When it finally arrived it was a punk, which is why it
looked like slow art rather than a slow lookup.

The punk is derived from the pubkey alone, so it can be drawn in the first
frame. It's already the right answer for everyone with no picture, and for a
coinos user who kept the default it IS their published picture — same
formula, same file. A real photo replaces it when it lands. A name lookup in
flight still gets the quiet circle: a picture is expected there, and punk art
must not flash in front of it.

Also: the punk rewrite now recognises the bare relative path a few older
profiles carry, not just the absolute URL.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Ship the punks at the size we actually draw them.</title>
<updated>2026-09-11T05:51:56Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-11T05:51:56Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=82cd8da7f5ab285b3a91f7284fb8d845400012ca'/>
<id>urn:sha1:82cd8da7f5ab285b3a91f7284fb8d845400012ca</id>
<content type='text'>
The art is 240x240 (20KB each, 1.5MB for the set) because that's what a punk
published as someone's nostr picture should be — but the circles the app
draws are 18 to 64px, so a new wallet with nothing cached was fetching eight
times the pixels it could show, one request per face.

punks-sm carries a 128px copy of each: 4KB instead of 20KB, indistinguishable
at 64px. Small circles and the wizard's picker tiles load those; only the 64px
profile avatar and the value the wizard publishes stay full-size.

The bigger win is that a punk picture is our own art wherever it turns up.
Everyone who keeps the default publishes v3.coinos.io/punks/N.webp as their
nostr picture, so those faces now paint straight off disk at the size being
drawn — no request, no thumbnail to make, and no broken circle when the
legacy coinos.io copies 502 (which they do for a third of the set).

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Keep a thumbnail of every face, so they're on screen in the first frame.</title>
<updated>2026-09-11T05:47:05Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-11T05:47:05Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=ac675185e362930f2086e01ab578fdd3423e4c79'/>
<id>urn:sha1:ac675185e362930f2086e01ab578fdd3423e4c79</id>
<content type='text'>
A profile picture is whatever its owner uploaded, and that is very often the
full-size original. Measured against the faces this wallet had cached: one
avatar is a 4,988,208-byte JPEG, another 3,901,966 — megabytes fetched and
decoded to paint a 30px circle. That is the second of blank circles on the
messages page after a refresh, and it repeats every time the upstream cache
header expires.

So the first time we see a picture we downscale it once (96px, webp) and keep
the thumbnail beside the profile in localStorage, which is read synchronously
at boot. Small avatars then paint from it alone and never ask the network for
the original again; the 64px profile avatar layers them, so the thumbnail
shows instantly while the full-size original loads over the top.

Reading the pixels needs CORS — most avatar hosts allow it (6 of the 9 here),
and the ones that don't are remembered as failures rather than retried every
boot. A refresh of the profile carries the thumbnail across when the picture
hasn't changed. Thumbnails live on a 180k budget, oldest face first, and Data
Saver opts out of making them at all.

tools/avatar-thumb-test.js watches a wallet meet a large cross-origin picture
and checks the next boot fetches no picture at all.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
</feed>
