aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.js23
-rw-r--r--src/features/ark.js20
-rw-r--r--src/features/messages.js4
-rw-r--r--src/style.css17
4 files changed, 53 insertions, 11 deletions
diff --git a/src/app.js b/src/app.js
index ffe9ba8..8d73b71 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1209,31 +1209,44 @@ let unit = (() => {
// been fetched — a third position showing blanks is worse than two that work.
function toggleUnit() {
const ring = haveRate() ? ['sats', 'btc', 'fiat'] : ['sats', 'btc'];
+ const was = unit;
unit = ring[(ring.indexOf(unit) + 1) % ring.length];
try {
localStorage.setItem(UNIT_KEY, unit);
} catch {}
+ // An amount already typed into the send form is the same money in the new
+ // unit, not the same digits: 1000 sats does not become 1000 BTC. Whatever
+ // doesn't parse is left as typed.
+ for (const r of (ui.send && ui.send.recipients) || []) {
+ if (!r.amount || !String(r.amount).trim()) continue;
+ const sat = parseField(r.amount, was);
+ if (sat) r.amount = amountField(sat);
+ }
render();
}
// A sats figure as the amount field types it in the current unit — what a
// Max button or a fixed-amount invoice fills in. Fiat is floored to the
// cent so a filled-in maximum never parses back to more than was offered.
-// The figure it filled is remembered with its exact sats: a fiat maximum
+// The figures it filled are remembered with their exact sats: a fiat maximum
// rounded to the cent would otherwise parse back a few sats short and leave
-// them behind. Typing anything else parses as typed.
-let lastFill = null;
+// them behind. Typing anything else parses as typed. Several are kept so a
+// form with more than one amount round-trips each of them.
+const fills = new Map();
function amountField(sat) {
let s;
const r = unit === 'fiat' && rateNow();
if (unit === 'sats') s = String(sat);
else if (r) s = (Math.floor((sat / 1e8) * r * 100) / 100).toFixed(2);
else s = (sat / 1e8).toFixed(8);
- lastFill = { s, unit, sat };
+ fills.delete(unit + '|' + s);
+ fills.set(unit + '|' + s, sat);
+ while (fills.size > 16) fills.delete(fills.keys().next().value);
return s;
}
function parseField(v, u = unit) {
- if (lastFill && lastFill.unit === u && String(v).trim() === lastFill.s) return lastFill.sat;
+ const k = u + '|' + String(v).trim();
+ if (fills.has(k)) return fills.get(k);
return parseAmount(v, u, rateNow());
}
diff --git a/src/features/ark.js b/src/features/ark.js
index eccc3b8..739dd5e 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -2677,7 +2677,7 @@ export function arkFeature(ctx) {
const view = arkView();
const info = (view && view.info) || (cachedArkInfo(cfg && cfg.ark) || {}).info || null;
const honors = !!(cfg && cfg.honorsExpired);
- const cur = honors ? getCustody() : 'self';
+ const cur = getCustody(); // never 'custodial' on a server that doesn't honor expired coins
const base = (info && info.refreshFees && info.refreshFees.baseFeeSat) || 0;
const table = (info && info.refreshFees && info.refreshFees.ppmExpiryTable) || [];
const ppmAt = (blocks) => (table.filter((e) => e.thresholdBlocks <= blocks).pop()?.ppm) ?? 0;
@@ -3402,10 +3402,20 @@ export function arkFeature(ctx) {
// happening anyway, for next to nothing; the Manage page consolidates on
// request.
const CUSTODY_DEFAULT = 'custodial';
+ // The tier in force: the stored choice, except that "coinos holds my coins"
+ // exists only on a server that honors a coin past its timelock. Anywhere
+ // else (Second, a custom captaind) an expired coin is gone, so renewal is
+ // automatic there whatever is stored — the stored default stays on the
+ // record for a later switch back to a coinos server. From 2026-10-22 to
+ // 10-23 the stored default applied everywhere, and a wallet on such a
+ // server renewed nothing and warned of nothing, in the app and the worker.
function getCustody() {
let v = wallet.loadFeatureState ? wallet.loadFeatureState('arkCustody', null) : null;
if (v === 'early') v = 'exit'; // the third tier's first name (one day in October 2026)
- return CUSTODY_TIERS.includes(v) ? v : CUSTODY_DEFAULT;
+ const tier = CUSTODY_TIERS.includes(v) ? v : CUSTODY_DEFAULT;
+ if (tier !== 'custodial') return tier;
+ const cfg = getArkConfig();
+ return cfg && !cfg.honorsExpired ? 'self' : tier;
}
function setCustody(tier) { if (wallet.saveFeatureState) wallet.saveFeatureState('arkCustody', tier); }
const custodyPolicy = () => renewPolicy(getCustody(), getNetwork());
@@ -3416,8 +3426,10 @@ export function arkFeature(ctx) {
// (there is no cancel; a spent input voids one) — they were priced when
// registered and renew one coin each, once.
function applyCustody(tier) {
- if (!CUSTODY_TIERS.includes(tier) || tier === getCustody()) return;
- setCustody(tier);
+ if (!CUSTODY_TIERS.includes(tier)) return;
+ const same = tier === getCustody();
+ setCustody(tier); // the record keeps the choice even when it is already in force here
+ if (same) return;
arkAutoRefreshAt = 0; arkBackstopAt = 0; arkRenewWarn = null;
if (ark && ark.state) maybeAutoRefresh(ark).catch(() => {});
writeBgMirror().catch(() => {});
diff --git a/src/features/messages.js b/src/features/messages.js
index 030d9fa..dea454c 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -9309,7 +9309,7 @@ export function messagesFeature(ctx) {
};
return h('div', { class: 'col live-page', style: 'gap:12px' },
ctx.brandHeader(!ui.pubProf && wallet.loaded),
- h('div', { class: 'card col', style: 'gap:10px' },
+ h('div', { class: 'card col live-main', style: 'gap:10px' },
h('div', { class: 'row gap6', style: 'align-items:center' },
mine ? h('button', { class: 'iconbtn chat-back', onClick: () => { if (confirm(t('goLiveEndAsk'))) endBroadcast(); } }, '‹')
: backBtn(() => { ui.liveStream = null; }),
@@ -9361,7 +9361,7 @@ export function messagesFeature(ctx) {
watchersRow(w),
liveSummary(ev) ? h('div', { class: 'small muted', style: 'white-space:pre-wrap;overflow-wrap:anywhere' }, liveSummary(ev).slice(0, 600)) : null,
ui.liveReport && ui.liveReport.a === s.a ? liveReportPanel(ev) : null),
- h('div', { class: 'card col', style: 'gap:8px' },
+ h('div', { class: 'card col live-chat-card', style: 'gap:8px' },
h('div', { class: 'small muted' }, t('liveChat')),
h('div', {
class: 'live-chat', 'data-key': 'live-chat:' + s.a,
diff --git a/src/style.css b/src/style.css
index 275bb59..384b6d6 100644
--- a/src/style.css
+++ b/src/style.css
@@ -929,6 +929,23 @@ html.no-scroll { overflow: hidden; scrollbar-gutter: auto; }
.live-line-body { min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; line-height: 1.35; }
.live-zap .live-line-body b { color: var(--accent); }
.live-chat .chat-row { padding-right: 2px; }
+/* on phones the stream is the page, the way Nostrich lays one out: the
+ picture runs edge to edge and the chat under it is a plain full-width
+ list of lines, no bubbles — the card walls step outside #app's padding */
+@media (max-width: 520px) {
+ .live-page > .card {
+ margin-left: -16px; margin-right: -16px; padding: 10px 0 12px;
+ border-left: none; border-right: none; border-radius: 0; box-shadow: none;
+ }
+ .live-page > .live-chat-card { margin-top: 0; padding-top: 6px; border-top: none; }
+ .live-page > .card > :not(.live-player, .live-chat) { margin-left: 14px; margin-right: 14px; }
+ .live-page .live-player { border-radius: 0; }
+ .live-page .live-chat { padding: 10px 14px 0; max-height: 60vh; }
+ .live-page .live-title { font-size: 17px; }
+ .live-chat .chat-row { gap: 10px; }
+ .live-chat .chat-bubble { display: block; background: transparent; border: 0; border-radius: 0; padding: 0; font-size: 15px; }
+ .live-chat .chat-quote { margin-bottom: 2px; }
+}
.bc-preview.mirror { transform: scaleX(-1); }
.bc-bar .bc-clock { font-variant-numeric: tabular-nums; font-weight: 650; }
.go-live-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #e0245e; box-shadow: 0 0 0 3px rgba(224, 36, 94, .25); }