aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-10 22:51:56 -0700
committerAdam Soltys <[email protected]>2026-09-10 22:51:56 -0700
commit82cd8da7f5ab285b3a91f7284fb8d845400012ca (patch)
treeebf9d6d638ba110ff1e17624148ef7d9ac7412b5 /src/features
parentac675185e362930f2086e01ab578fdd3423e4c79 (diff)
downloadcoinosv3-82cd8da7f5ab285b3a91f7284fb8d845400012ca.tar.gz
coinosv3-82cd8da7f5ab285b3a91f7284fb8d845400012ca.zip
Ship the punks at the size we actually draw them.
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) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js34
1 files changed, 28 insertions, 6 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 18703ab..b23d7ad 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -332,6 +332,17 @@ export function messagesFeature(ctx) {
}
wallet.saveFeatureState('profiles', s);
}
+ // A punk picture is OUR OWN art. Every coinos user who keeps the default
+ // publishes https://v3.coinos.io/punks/N.webp as their nostr picture, and
+ // the legacy coinos.io copies 502 for a third of the set — so painting
+ // those from the local files means no network, no broken faces, and the
+ // right size for the circle being drawn.
+ const PUNK_PIC_RE = /^https?:\/\/(?:[a-z0-9-]+\.)*coinos\.io\/punks\/(\d{1,2})\.webp$/i;
+ const localPunk = (url, big) => {
+ const m = PUNK_PIC_RE.exec(url || '');
+ return m ? (big ? `punks/${m[1]}.webp` : `punks-sm/${m[1]}.webp`) : null;
+ };
+
// A refreshed profile whose picture hasn't changed keeps the thumbnail we
// already made of it — otherwise every relay refresh throws the local copy
// away and the original gets downloaded all over again.
@@ -367,6 +378,7 @@ export function messagesFeature(ctx) {
function makeThumb(pk, p) {
if (!p || !p.picture || typeof document === 'undefined') return;
if (p.thumbFor === p.picture || p.thumbFail === p.picture) return;
+ if (localPunk(p.picture)) return; // our own art, already the right size on disk
if (thumbing.has(pk) || thumbing.size >= 3) return; // a few at a time
// Making the thumbnail costs one more fetch of the original today to
// save every fetch after it — but not on a connection someone is
@@ -1732,6 +1744,21 @@ export function messagesFeature(ctx) {
' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
};
+ // Which bytes a face paints from, in order of preference:
+ // our own punk art, straight off disk at the size being drawn;
+ // the thumbnail we keep locally, which needs no network at all;
+ // the original, when we have nothing else yet.
+ // The 64px profile avatar layers the original OVER the local copy: CSS
+ // paints the first layer that has arrived, so the face is there instantly
+ // and sharpens when the full-size one lands.
+ function avatarBg(p, big) {
+ const punk = localPunk(p.picture, big);
+ if (punk) return `url(${JSON.stringify(punk)})`;
+ const local = p.thumb && p.thumbFor === p.picture ? p.thumb : null;
+ if (!local) return `url(${JSON.stringify(p.picture)})`;
+ return (big ? `url(${JSON.stringify(p.picture)}),` : '') + `url(${JSON.stringify(local)})`;
+ }
+
const avatar = (pk, cls = 'chat-avatar', clickable = true) => {
const p = profileOf(pk);
// While the profile is in flight, a quiet empty circle — the punk is a
@@ -1748,12 +1775,7 @@ export function messagesFeature(ctx) {
// 64px profile avatar layers them: CSS paints the first layer that
// has arrived, so the thumbnail shows instantly and the full-size
// original takes over on top when it lands.
- ? h('div', {
- class: cls + ' ava-img',
- style: 'background-image:' + (p.thumb && p.thumbFor === p.picture
- ? (cls.includes('profile-avatar') ? `url(${JSON.stringify(p.picture)}),` : '') + `url(${JSON.stringify(p.thumb)})`
- : `url(${JSON.stringify(p.picture)})`),
- })
+ ? h('div', { class: cls + ' ava-img', style: 'background-image:' + avatarBg(p, cls.includes('profile-avatar')) })
: fallbackAvatar(h, pk, p.name, cls);
// A face we keep painting is one worth keeping a thumbnail of.
if (p && p.picture) makeThumb(pk, p);