diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 37 | ||||
| -rw-r--r-- | src/features/ark.js | 22 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | src/rates.js | 17 |
4 files changed, 44 insertions, 33 deletions
@@ -15,7 +15,7 @@ import { dataSources, getSource, setSource, getNetwork, setNetwork, NETWORKS } f import { STAGING } from './build-flags.js'; import { buildFeatures, loadDeferredFeatures } from './features/index.js'; import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js'; -import { rateNow, rateAt, refreshRates, haveRate, getCurrency, setCurrency, currencies, fmtFiat } from './rates.js'; +import { rateNow, rateAt, refreshRates, haveRate, getCurrency, setCurrency, currencies, fmtFiat, fmtFiatBare } from './rates.js'; import { fmtBtc, fmtSats, @@ -274,7 +274,10 @@ function footer() { h('span', { class: 'faint' }, ' · '), h('button', { class: 'linklike', style: 'font-weight:400', onClick: toggleTheme }, resolvedTheme() === 'dark' ? t('lightMode') : t('darkMode')) ), - h('div', { style: 'margin-top:8px' }, languagePicker()), + // Display preferences live together: the language this reads in, and the + // money amounts are shown in. (The currency sat under Settings → Network + // for exactly one day, where nobody found it.) + h('div', { class: 'row gap6', style: 'margin-top:8px;justify-content:center' }, languagePicker(), currencyPicker()), // The commit stamp settles "is my PWA fresh?" AND "what code is this?" in // one glance — its own last line, so it never wraps the credits on mobile. h('div', { class: 'faint', style: 'margin-top:6px' }, @@ -823,6 +826,9 @@ function worthLine(sats, at, row = null) { const now = rateNow(); if (!now || !at) return null; const line = row || ((k, v) => h('div', { class: 'line' }, h('span', { class: 'k' }, k), h('span', { class: 'v' }, v))); + // A payment we have a price for is shown at that price; one we don't is + // shown at today's, without ceremony. (The '~' that used to mark the + // difference was more bookkeeping than the beta warrants.) const thenStr = then.rate && !then.stale ? fmtFiat(sats, then.rate) : null; const nowStr = fmtFiat(sats, now); if (!thenStr) return line(t('worthNow'), nowStr); @@ -837,9 +843,11 @@ function worthLine(sats, at, row = null) { const fmtAmount = (sats, at) => { if (unit === 'sats') return fmtSats(sats); if (unit === 'btc') return fmtBtc(sats); - const { rate, stale } = rateAt(at); + const { rate } = rateAt(at); if (!rate) return fmtSats(sats); - return (stale && at ? '~' : '') + fmtFiat(sats, rate); + // bare, like the other two units: whatever labels this number — the unit + // tag beside a balance, the toggle everywhere else — says which money it is + return fmtFiatBare(sats, rate); }; // A clickable unit label. cls lets callers inherit surrounding sizing. @@ -2329,7 +2337,7 @@ function settingsTab() { switch (ui.settingsPage) { case 'wallet': return page(a ? [walletNameCard(a), pubkeyCard(a), recoveryCard(a)] : []); case 'payments': return page(featureAll('settingsCards').reverse()); - case 'network': return page([networkCard(), currencyCard(), explorerCard()]); + case 'network': return page([networkCard(), explorerCard()]); case 'nostr': return nostrSettingsView(); case 'notifications': return page(featureAll('notifySettingsCards').reverse()); case 'advanced': return advancedSettingsView(); @@ -2493,19 +2501,12 @@ function explorerCard() { // Changing it starts the recorded price history again: the samples behind // "worth then" are denominated in the currency they were taken in, and // relabelling them would be a lie. -function currencyCard() { - const codes = currencies(); - return h( - 'div', - { class: 'card col' }, - h('h3', {}, t('currencyLabel')), - h('select', { - value: getCurrency(), - onChange: (e) => { setCurrency(e.target.value); refreshRates({ force: true }).then(render); render(); }, - }, codes.map((c) => h('option', { value: c, selected: c === getCurrency() }, c))), - h('div', { class: 'small faint' }, t('currencyHint')), - rateNow() ? h('div', { class: 'small muted' }, '1 BTC = ' + fmtFiat(SATS, rateNow())) : null - ); +function currencyPicker() { + return h('select', { + title: t('currencyLabel'), + value: getCurrency(), + onChange: (e) => { setCurrency(e.target.value); refreshRates({ force: true }).then(render); render(); }, + }, currencies().map((c) => h('option', { value: c, selected: c === getCurrency() }, c))); } // Language selector. Changing it persists the choice, flips text direction for diff --git a/src/features/ark.js b/src/features/ark.js index e460b4c..764affd 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -20,7 +20,7 @@ import { import { t } from '../i18n.js'; import { resolveBip353, parsePaymentName, parseBip21 as parseBip21Uri } from '../bip353.js'; import { parseZapTarget, fetchPayParams, requestInvoice } from '../lnurl.js'; -import { shortAddr, shortTxid, timeAgo, ARK_ICON, ARK_MARK, BITCOIN_ICON } from '../format.js'; +import { shortAddr, shortTxid, timeAgo, fmtSats, ARK_ICON, ARK_MARK, BITCOIN_ICON } from '../format.js'; // t?ark1… bech32m — an Ark address for this or another ASP. export function isArkAddress(a) { return /^t?ark1[a-z0-9]{20,}$/i.test((a || '').trim()); } @@ -1312,7 +1312,7 @@ export function arkFeature(ctx) { h('span', { class: 'tag pending' }, t('arkExitPendingTag'))), h('div', { class: 'amount-neg', style: 'font-size:20px' }, '-' + fmtAmount(a.amountSat) + ' ' + unitLabel()), row(t('dateLabel'), new Date(exitStartedTs(a)).toLocaleString()), - h('div', { class: 'small muted' }, t('arkExitSummary', { n: fmtAmount(a.amountSat), eta })), + h('div', { class: 'small muted' }, t('arkExitSummary', { n: fmtSats(a.amountSat), eta })), a.lastError ? h('div', { class: a.actionable ? 'small err' : 'small faint' }, a.actionable ? a.lastError : t('arkExitRetrying')) : null, url ? h('a', { class: 'btn btn-sm', href: url, target: '_blank', rel: 'noopener' }, t('arkExitViewTx')) : null, cancellable ? h('button', { class: 'linklike small', onClick: () => { @@ -1952,7 +1952,7 @@ export function arkFeature(ctx) { const EXIT_DEPTH_ADVISORY = 4; const depthNoticeEl = (exitFee) => h('div', { class: 'small muted', style: 'margin:10px 0 0;text-align:center' }, - t('arkDepthNotice', { fee: fmtAmount(exitFee) }), + t('arkDepthNotice', { fee: fmtSats(exitFee) }), ' ', h('button', { class: 'linklike small', onClick: () => { ui.arkCoinsPage = true; render(); } }, t('arkDepthBtn'))); @@ -2125,7 +2125,7 @@ export function arkFeature(ctx) { h('div', { class: 'card col', style: 'gap:8px' }, h('h4', { style: 'margin:0' }, t('arkCoinsExitTitle')), h('p', { class: 'small muted', style: 'margin:0' }, - t('arkCoinsExitDesc', { fee: fmtAmount(exitFee), after: fmtAmount(afterFee) }))), + t('arkCoinsExitDesc', { fee: fmtSats(exitFee), after: fmtSats(afterFee) }))), h('div', { class: 'card col', style: 'gap:8px' }, h('h4', { style: 'margin:0' }, t('arkCoinsRenewTitle')), h('p', { class: 'small muted', style: 'margin:0' }, t('arkCoinsRenewDesc')), @@ -2288,7 +2288,7 @@ export function arkFeature(ctx) { // one Savings can't carry, with the number on the table. const feeEst = estimateExitFeeSat(mgr); if ((wallet.spendable || 0) < feeEst) { - throw new Error(t('arkExitFeeShort', { need: fmtAmount(feeEst), have: fmtAmount(wallet.spendable || 0) })); + throw new Error(t('arkExitFeeShort', { need: fmtSats(feeEst), have: fmtSats(wallet.spendable || 0) })); } for (const v of spendables) mgr.startExit(v.id); toast(t('arkExitStarted', { n: spendables.length })); @@ -2343,7 +2343,7 @@ export function arkFeature(ctx) { mgr._save(); return; } catch { - const e = new Error(t('arkExitNoFeeCoin', { need: fmtAmount(Math.max(294, feeSat)) })); + const e = new Error(t('arkExitNoFeeCoin', { need: fmtSats(Math.max(294, feeSat)) })); e.actionable = true; throw e; } @@ -2566,13 +2566,13 @@ export function arkFeature(ctx) { // small balance: Lightning carries no mining fees at all. if (feeEst >= balance) { return h('div', { class: 'col', style: 'gap:8px;border-top:1px solid var(--border,rgba(128,128,128,.2));padding-top:10px' }, - h('div', { class: 'small muted' }, t('arkUniUneconomical', { fee: fmtAmount(feeEst), n: fmtAmount(balance) }))); + h('div', { class: 'small muted' }, t('arkUniUneconomical', { fee: fmtSats(feeEst), n: fmtSats(balance) }))); } const savings = wallet.spendable || 0; const short = savings < feeEst; return h('div', { class: 'col', style: 'gap:8px;border-top:1px solid var(--border,rgba(128,128,128,.2));padding-top:10px' }, - h('div', { class: 'small muted' }, t('arkUniFallbackBody', { fee: fmtAmount(feeEst) })), - short ? h('div', { class: 'small err' }, t('arkUniFallbackShort', { need: fmtAmount(feeEst), have: fmtAmount(savings) })) : null, + h('div', { class: 'small muted' }, t('arkUniFallbackBody', { fee: fmtSats(feeEst) })), + short ? h('div', { class: 'small err' }, t('arkUniFallbackShort', { need: fmtSats(feeEst), have: fmtSats(savings) })) : null, h('button', { class: 'btn-block', disabled: !!ui.arkBusy || short, onClick: () => { @@ -2581,7 +2581,7 @@ export function arkFeature(ctx) { ui.tab = 'history'; // the exit lives as a pending record there render(); }, - }, t('arkUniFallbackBtn', { fee: fmtAmount(feeEst) }))); + }, t('arkUniFallbackBtn', { fee: fmtSats(feeEst) }))); })()); } @@ -2963,7 +2963,7 @@ export function arkFeature(ctx) { // batch, gone the moment it would be a lie. const cancellable = acts.every((a) => a.actionable && !(a.hopsDone > 0) && a.step === 'chain'); return [h('div', { class: 'small muted', style: 'margin-top:4px' }, - t('arkExitSummary', { n: fmtAmount(totalSat), eta }), + t('arkExitSummary', { n: fmtSats(totalSat), eta }), err ? h('div', { class: 'small err' }, err.lastError) : softErr ? h('div', { class: 'small faint' }, t('arkExitRetrying')) : null, watch ? h('div', { style: 'margin-top:4px' }, diff --git a/src/i18n.js b/src/i18n.js index df7c5c8..9aa398e 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -197,7 +197,6 @@ const STR = { worthNow: 'Worth now', worthNowInline: '{v} now', currencyLabel: 'Show amounts in', - currencyHint: 'The third tap on any amount switches to this. Payments are shown at the price when they happened.', feedTitle: 'Feed', feedFollow: 'Follow', feedFollowing: 'Following', diff --git a/src/rates.js b/src/rates.js index bc32bd7..19b3512 100644 --- a/src/rates.js +++ b/src/rates.js @@ -116,13 +116,24 @@ export async function refreshRates({ force = false } = {}) { // it does — a third position that shows blanks is worse than two that work. export const haveRate = () => rateNow() != null; +// Two shapes. `fmtFiat` carries its own currency symbol, for a line of prose +// that has to say what the number is. `fmtFiatBare` is just the number, for +// the places where the unit is already spelled out beside it — a balance +// under its USD tag reading "US$74.67 USD" says it twice. export function fmtFiat(sats, rate, code = getCurrency()) { if (!rate) return ''; const v = (Number(sats) / 100_000_000) * rate; - const digits = Math.abs(v) < 1 && v !== 0 ? 4 : 2; try { - return v.toLocaleString(undefined, { style: 'currency', currency: code, maximumFractionDigits: digits, minimumFractionDigits: 2 }); + return v.toLocaleString(undefined, { style: 'currency', currency: code, maximumFractionDigits: fiatDigits(v), minimumFractionDigits: 2 }); } catch { - return v.toFixed(digits) + ' ' + code; // a code Intl doesn't know + return fmtFiatBare(sats, rate) + ' ' + code; // a code Intl doesn't know } } +export function fmtFiatBare(sats, rate) { + if (!rate) return ''; + const v = (Number(sats) / 100_000_000) * rate; + return v.toLocaleString(undefined, { maximumFractionDigits: fiatDigits(v), minimumFractionDigits: 2 }); +} +// Under a unit of currency, sats are small: a tenth of a cent still deserves +// to be a number rather than 0.00. +const fiatDigits = (v) => (Math.abs(v) < 1 && v !== 0 ? 4 : 2); |
