aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.js37
-rw-r--r--src/features/ark.js22
-rw-r--r--src/i18n.js1
-rw-r--r--src/rates.js17
4 files changed, 44 insertions, 33 deletions
diff --git a/src/app.js b/src/app.js
index 6b481a5..5aabd57 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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);