diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 6 | ||||
| -rw-r--r-- | src/features/messages.js | 34 | ||||
| -rw-r--r-- | src/recipient-search.js | 17 |
3 files changed, 47 insertions, 10 deletions
@@ -2904,7 +2904,11 @@ function shouldOnboard() { } const PUNK_PICKS = [7, 14, 21, 3, 33, 40, 47, 36, 61, 26, 12, 50]; +// The value the wizard publishes is always the full-size art — that URL +// becomes this person's picture on every nostr client. The tiles in the +// picker are 56px, so they load the small copies. const punkImg = (n) => `punks/${n}.webp`; +const punkTile = (n) => `punks-sm/${n}.webp`; async function onbUpload(file) { const fd = new FormData(); @@ -3106,7 +3110,7 @@ function onboardScreen() { ...PUNK_PICKS.map((n) => h('img', { class: 'onb-punk' + (sel === punkImg(n) ? ' sel' : ''), - src: punkImg(n), alt: '', + src: punkTile(n), alt: '', onClick: () => { o.avatar = punkImg(n); render(); }, onError: (e) => { e.target.remove(); }, // a 502'd punk removes itself }))), 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); diff --git a/src/recipient-search.js b/src/recipient-search.js index a5c5f46..be3c70e 100644 --- a/src/recipient-search.js +++ b/src/recipient-search.js @@ -15,12 +15,23 @@ import { queryOn, parseNostrPubkey, npubOf, PROFILE_RELAYS } from './nostr.js'; // if the image can't load (offline). // Self-hosted (dist/punks) — coinos.io's own copies 502 for a third of the // set. Same deterministic formula the coinos app uses. -export const punkUrl = (pk) => - `punks/${Math.floor((parseInt(pk.slice(-2), 16) / 256) * 64) + 1}.webp`; +const punkN = (pk) => Math.floor((parseInt(pk.slice(-2), 16) / 256) * 64) + 1; +export const punkUrl = (pk) => `punks/${punkN(pk)}.webp`; +// The art is 240px, which is what a punk published as someone's nostr +// picture should be — and eight times what a 30px circle needs. We ship a +// 128px copy of each (a fifth of the bytes, 20KB → 4KB) and paint the circles +// from that, so a brand-new wallet with nothing cached fills its faces in +// straight away. Only the 64px profile avatar takes the full-size one. +export const punkSmallUrl = (pk) => `punks-sm/${punkN(pk)}.webp`; +export const punkSmallN = (n) => `punks-sm/${n}.webp`; export const fallbackAvatar = (h, pk, name, cls) => h('div', { class: cls + ' fallback' }, - h('img', { class: 'punk', src: punkUrl(pk), alt: '', onError: (e) => { e.target.style.display = 'none'; } }), + h('img', { + class: 'punk', alt: '', + src: cls.includes('profile-avatar') ? punkUrl(pk) : punkSmallUrl(pk), + onError: (e) => { e.target.style.display = 'none'; }, + }), (name || npubOf(pk) || '??').slice(0, 2)); const REGISTRAR = 'https://names.coinos.io'; |
