aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-14 00:16:20 -0700
committerAdam Soltys <[email protected]>2026-08-14 00:16:20 -0700
commit106254b74b28a5d5e1390bacabaf08e5613a86f4 (patch)
tree916c468b217f275d634e58e3916ec1517f945dc1
parent5abf76764c445b742a50080ebb97c313531d5962 (diff)
downloadcoinosv3-106254b74b28a5d5e1390bacabaf08e5613a86f4.tar.gz
coinosv3-106254b74b28a5d5e1390bacabaf08e5613a86f4.zip
Holding backspace works in the recipient field: search results and reveals stop rebuilding the focused input mid-keypress; the profile npub is itself the copy button
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
-rw-r--r--src/app.js38
-rw-r--r--src/features/messages.js12
2 files changed, 36 insertions, 14 deletions
diff --git a/src/app.js b/src/app.js
index a509241..5d0264d 100644
--- a/src/app.js
+++ b/src/app.js
@@ -3028,7 +3028,8 @@ function destReady(a) {
// Recipient search under the destination input: usernames and npubs resolve
// to candidates with avatars; picking one runs the same path as pasting.
-const sendSearch = { rows: null };
+const sendSearch = { rows: null, sync: null };
+let sendRevealTimer = null;
// With the phone keyboard up, the space under the recipient field is scarce —
// park the field at the top of the view so the candidate list gets what's
// left. Runs on focus and again whenever fresh candidates land (the list
@@ -3039,7 +3040,15 @@ function parkSendField() {
const el = document.activeElement;
if (el && el.isConnected && el.tagName === 'INPUT') el.scrollIntoView({ block: 'start', behavior: 'smooth' });
}
-const sendSearcher = makeSearcher((q, rows) => { sendSearch.rows = rows; render(); if (rows && rows.length) setTimeout(parkSendField, 50); });
+// Results update the panel IMPERATIVELY (sendSearch.sync, set by the mounted
+// recipient row) rather than via render(): a full render swaps the focused
+// input for a fresh node, which resets Android's keyboard session and cancels
+// key auto-repeat — holding backspace deleted one character per press.
+const sendSearcher = makeSearcher((q, rows) => {
+ sendSearch.rows = rows;
+ if (sendSearch.sync) sendSearch.sync(); else render();
+ if (rows && rows.length) setTimeout(parkSendField, 50);
+});
// One recipient: address + amount. Max is only offered for a single recipient.
function recipientRow(s, r, i) {
@@ -3059,6 +3068,16 @@ function recipientRow(s, r, i) {
check.replaceChildren(...nodes);
check.style.display = a ? '' : 'none';
};
+ // The suggestions panel gets the same imperative treatment — see the
+ // sendSearcher note: a render between key repeats kills backspace-hold.
+ const suggest = i === 0 ? h('div', { class: 'list send-suggest', style: 'display:none' }) : null;
+ const syncSuggest = () => {
+ if (!suggest) return;
+ const show = sendSearch.rows && sendSearch.rows.length && searchable(r.address);
+ suggest.replaceChildren(...(show ? resultRows(h, sendSearch.rows, pickRecipient) : []));
+ suggest.style.display = show ? '' : 'none';
+ };
+ if (i === 0) sendSearch.sync = syncSuggest;
// A pasted/typed/scanned payload a feature recognizes (e.g. a bolt11) jumps
// straight to that feature's own confirmation flow.
@@ -3077,8 +3096,14 @@ function recipientRow(s, r, i) {
if (i === 0) sendSearcher.update(v); // candidates render when results land
if (tryFeature(v, true)) return; // a bolt11 advances to its own confirmation
// reveal/hide amount + controls as validity flips, an annotation
- // (zap button) appears/disappears, or an error clears
- if (hadError || destReady(v) !== r._ready || !!featureHook('sendFormNote', v) !== r._note) render();
+ // (zap button) appears/disappears, or an error clears — but only after
+ // typing pauses: an immediate render swaps this input for a fresh node,
+ // and Android cancels key auto-repeat when the focused field resets
+ // (holding backspace deleted a single character).
+ if (hadError || destReady(v) !== r._ready || !!featureHook('sendFormNote', v) !== r._note) {
+ clearTimeout(sendRevealTimer);
+ sendRevealTimer = setTimeout(render, 250);
+ }
},
});
const pickRecipient = (cand) => {
@@ -3104,9 +3129,7 @@ function recipientRow(s, r, i) {
!single && h('button', { type: 'button', class: 'btn-sm', title: t('remove'), onClick: () => { s.recipients.splice(i, 1); render(); } }, '✕')
),
check,
- i === 0 && sendSearch.rows && sendSearch.rows.length && searchable(r.address)
- ? h('div', { class: 'list send-suggest' }, resultRows(h, sendSearch.rows, pickRecipient))
- : null,
+ suggest,
r._ready ? h('div', { class: 'input-group' },
h('input', {
type: 'number', step: unit === 'sats' ? '1' : '0.00000001', min: '0',
@@ -3133,6 +3156,7 @@ function recipientRow(s, r, i) {
) : null
);
syncCheck();
+ syncSuggest();
return row;
}
diff --git a/src/features/messages.js b/src/features/messages.js
index 598e661..a27a6b7 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -1166,13 +1166,11 @@ export function messagesFeature(ctx) {
: showAbout && !ui.profEdit ? h('p', { class: 'small', style: 'margin:0;white-space:pre-wrap' }, about.slice(0, 1000)) : null,
// your own npub stays out of the editor — it means nothing to most
// people, and the account settings still show it to those who care
- mine ? null : h('div', { class: 'addr-box break npub-box', style: 'font-size:11px' },
- h('span', { class: 'grow', style: 'min-width:0' }, npub),
- h('button', {
- class: 'copy-ico', title: t('copy'),
- onClick: async () => { try { await navigator.clipboard.writeText(npub); toast(t('copied')); } catch {} },
- html: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
- })),
+ mine ? null : h('button', {
+ class: 'addr-box break npub-box', title: t('copy'),
+ style: 'font-size:11px;cursor:pointer;text-align:left;width:100%',
+ onClick: async () => { try { await navigator.clipboard.writeText(npub); toast(t('copied')); } catch {} },
+ }, npub),
ui.profEdit
? h('div', { class: 'col', style: 'gap:8px' },
field(t('profName'), 'name'),