From 66d41d1aa1f5c60cd63b1691ba6bf70e88cf78fa Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 1 Oct 2026 16:34:55 -0700 Subject: The standalone wallet opened from a disk shows its avatars, font and languages. With no files beside it, relative punks/, locales/, nip46.js and jsqr.js looked on the disk. From file:// they now come from v3.coinos.io (images and scripts need no CORS; /locales/ now sends Access-Control-Allow-Origin on the server), and standalone.html carries Public Sans inline. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog --- src/app.js | 5 +++-- src/asset-url.js | 6 ++++++ src/i18n.js | 3 ++- src/nostr-login.js | 3 ++- src/recipient-search.js | 4 ++-- src/scan.js | 3 ++- 6 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 src/asset-url.js (limited to 'src') diff --git a/src/app.js b/src/app.js index dc17a37..54404d4 100644 --- a/src/app.js +++ b/src/app.js @@ -8,6 +8,7 @@ import { Wallet, newMnemonic, isValidMnemonic, accountXpubFor, cacheKeyFor, utxo import { qrSvg } from './qr.js'; import './rich-text.js'; // , the composers' field import { makeSearcher, resultRows, searchable, punkUrl, warmSearch } from './recipient-search.js'; +import { assetUrl } from './asset-url.js'; import { npubOf, seedPubkey, neventOf } from './nostr.js'; import { nip98Header } from './nip98.js'; import { uploadPublicMedia, MediaUploadError } from './media-upload.js'; @@ -3467,7 +3468,7 @@ const PUNK_PICKS = [7, 14, 21, 3, 33, 40, 47, 36, 61, 26, 12, 50]; // 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`; +const punkTile = (n) => assetUrl(`punks-sm/${n}.webp`); async function onbUpload(file) { const fd = new FormData(); @@ -3752,7 +3753,7 @@ function onboardScreen() { title(t('onbAvatarTitle')), h('p', { class: 'muted', style: 'margin:0' }, t('onbAvatarBody')), preview ? h('img', { - class: 'onb-avatar', src: preview, alt: '', + class: 'onb-avatar', src: /^(https?|data|blob):/i.test(preview) ? preview : assetUrl(preview), alt: '', style: ui.onbBusy ? 'opacity:.5' : '', onError: (e) => { e.target.style.visibility = 'hidden'; }, }) : null, diff --git a/src/asset-url.js b/src/asset-url.js new file mode 100644 index 0000000..bd80240 --- /dev/null +++ b/src/asset-url.js @@ -0,0 +1,6 @@ +// The standalone wallet (standalone.html opened from a disk) has no files +// beside it: punks, the lazy modules and the language packs come from the +// site instead. Served from anywhere else, paths stay relative. +export const SITE = 'https://v3.coinos.io/'; +const fromDisk = () => typeof location !== 'undefined' && location.protocol === 'file:'; +export const assetUrl = (path) => fromDisk() ? SITE + path.replace(/^\//, '') : path; diff --git a/src/i18n.js b/src/i18n.js index 951bc8d..8d67b3a 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,3 +1,4 @@ +import { assetUrl } from './asset-url.js'; // Lightweight i18n. t('key', {vars}) interpolates {var} and falls back to the // English string (then the key). The chosen language persists in localStorage; // otherwise it's guessed from the browser. Arabic/Urdu render right-to-left. @@ -1307,7 +1308,7 @@ const _inflight = new Map(); export function loadLocale(code) { if (code === 'en' || STR[code] || !SUPPORTED.has(code)) return Promise.resolve(); if (_inflight.has(code)) return _inflight.get(code); - const p = fetch('locales/' + code + '.json') + const p = fetch(assetUrl('locales/' + code + '.json')) .then((r) => (r.ok ? r.json() : null)) .then((d) => { if (d) STR[code] = d; }) .catch(() => {}) diff --git a/src/nostr-login.js b/src/nostr-login.js index d6cb978..fe60261 100644 --- a/src/nostr-login.js +++ b/src/nostr-login.js @@ -40,6 +40,7 @@ import { getPublicKey, finalizeEvent, nip44 } from './nostr.js'; import { SimplePool } from 'nostr-tools/pool'; import { generateSecretKey } from 'nostr-tools/pure'; import { decode as nip19decode } from 'nostr-tools/nip19'; +import { assetUrl } from './asset-url.js'; // NIP-46 loads lazily from dist/nip46.js (same pattern as the QR decoder): // bunker logins are rare, and the module is heavy. Tests running outside a @@ -50,7 +51,7 @@ function loadNip46() { if (typeof globalThis !== 'undefined' && globalThis.__nip46) return (_nip46 = Promise.resolve(globalThis.__nip46)); _nip46 = new Promise((resolve, reject) => { const s = document.createElement('script'); - s.src = 'nip46.js'; + s.src = assetUrl('nip46.js'); s.onload = () => (window.__nip46 ? resolve(window.__nip46) : reject(new Error('signer module unavailable'))); s.onerror = () => { _nip46 = null; reject(new Error('could not load the remote-signer module')); }; document.head.appendChild(s); diff --git a/src/recipient-search.js b/src/recipient-search.js index e0339ca..20083ee 100644 --- a/src/recipient-search.js +++ b/src/recipient-search.js @@ -9,6 +9,7 @@ // prefixes already seen. import { queryOn, parseNostrPubkey, npubOf, PROFILE_RELAYS } from './nostr.js'; +import { assetUrl } from './asset-url.js'; // coinos punks: the deterministic default avatar — same formula the coinos // app uses (last byte of the pubkey picks one of 64). Initials show through @@ -16,8 +17,7 @@ import { queryOn, parseNostrPubkey, npubOf, PROFILE_RELAYS } from './nostr.js'; // Self-hosted (dist/punks) — coinos.io's own copies 502 for a third of the // set. Same deterministic formula the coinos app uses. const punkN = (pk) => Math.floor((parseInt(pk.slice(-2), 16) / 256) * 64) + 1; -export const punkImageUrl = (n, small = false) => - `${typeof location !== 'undefined' && location.protocol === 'file:' ? '' : '/'}punks${small ? '-sm' : ''}/${n}.webp`; +export const punkImageUrl = (n, small = false) => assetUrl(`/punks${small ? '-sm' : ''}/${n}.webp`); export const punkUrl = (pk) => punkImageUrl(punkN(pk)); // 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 diff --git a/src/scan.js b/src/scan.js index a0d520c..083d5bb 100644 --- a/src/scan.js +++ b/src/scan.js @@ -1,3 +1,4 @@ +import { assetUrl } from './asset-url.js'; // Camera QR scanner. Opens a fullscreen overlay, streams the back camera, and // resolves with the decoded string (or null if the user cancels). // @@ -16,7 +17,7 @@ function loadJsQR() { if (!jsqrLoader) { jsqrLoader = new Promise((resolve, reject) => { const s = document.createElement('script'); - s.src = 'jsqr.js'; + s.src = assetUrl('jsqr.js'); s.onload = () => (window.jsQR ? resolve(window.jsQR) : reject(new Error('decoder unavailable'))); s.onerror = () => reject(new Error('decoder unavailable')); document.head.appendChild(s); -- cgit v1.2.3