diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 23 | ||||
| -rw-r--r-- | src/features/ark.js | 20 | ||||
| -rw-r--r-- | src/features/messages.js | 4 | ||||
| -rw-r--r-- | src/style.css | 17 |
4 files changed, 53 insertions, 11 deletions
@@ -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); } |
