aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-02 17:51:37 -0700
committerAdam Soltys <[email protected]>2026-08-02 17:51:37 -0700
commitd33643e0683dfac4037c0b61d4aa823782f582e7 (patch)
treeff185690f42cbb07d3fa925b2b7e5f8db5e1e53c /src/app.js
parent64f96e031e6ea219cc03eb8aa2cc4d0cf7e1f4e4 (diff)
downloadcoinosv3-d33643e0683dfac4037c0b61d4aa823782f582e7.tar.gz
coinosv3-d33643e0683dfac4037c0b61d4aa823782f582e7.zip
Recipient search: usernames and npubs, in chat and the Send form
A shared engine (src/recipient-search.js) merges three ranked sources: an exact npub/hex paste, coinos usernames from the registrar's new /search prefix endpoint, and NIP-50 kind-0 full-text search on relay.nostr.band — the approach the major web clients take, since ordinary relays don't index profiles. Results dedupe by pubkey, fill avatars in one batched kind-0 fetch, and cache per query so narrowing never re-hits the network. Input is debounced 300ms and gated by a searchable() heuristic so addresses, invoices, and keys never trigger a search. Chat's New message panel narrows as you type and opens the thread on pick; the Send form shows the same candidates under the destination input and picking one runs the normal paste path (payment address → BIP-353, npub → zap). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js20
1 files changed, 20 insertions, 0 deletions
diff --git a/src/app.js b/src/app.js
index 5f3b353..322383f 100644
--- a/src/app.js
+++ b/src/app.js
@@ -6,6 +6,8 @@
import { Wallet, newMnemonic, isValidMnemonic, accountXpubFor, cacheKeyFor, utxoId, parseExtendedKey, xpubToZpub, encryptVault, decryptVault } from './wallet.js';
import { qrSvg } from './qr.js';
+import { makeSearcher, resultRows, searchable } from './recipient-search.js';
+import { npubOf } from './nostr.js';
import { scanQr } from './scan.js';
import { dataSources, getSource, setSource, getNetwork, setNetwork, NETWORKS } from './api.js';
import { buildFeatures } from './features/index.js';
@@ -2403,6 +2405,11 @@ function destReady(a) {
return !!a && (wallet.isOnchainAddress(a) || FEATURES.some((f) => f.isSendDest && f.isSendDest(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 sendSearcher = makeSearcher((q, rows) => { sendSearch.rows = rows; render(); });
+
// One recipient: address + amount. Max is only offered for a single recipient.
function recipientRow(s, r, i) {
const single = s.recipients.length === 1;
@@ -2433,12 +2440,22 @@ function recipientRow(s, r, i) {
r.address = v; syncCheck();
const hadError = !!ui.sendError;
ui.sendError = ''; // editing the destination starts over — drop stale errors
+ 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();
},
});
+ const pickRecipient = (cand) => {
+ const v = cand.address || npubOf(cand.pk);
+ sendSearcher.clear();
+ addrInput.value = v;
+ r.address = v;
+ ui.sendError = '';
+ syncCheck();
+ if (!featureMatchSend(v)) render();
+ };
const row = h(
'div',
{ class: 'col gap6' },
@@ -2452,6 +2469,9 @@ 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,
r._ready ? h('div', { class: 'input-group' },
h('input', {
type: 'number', step: unit === 'sats' ? '1' : '0.00000001', min: '0',