diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 17:51:37 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 17:51:37 -0700 |
| commit | d33643e0683dfac4037c0b61d4aa823782f582e7 (patch) | |
| tree | ff185690f42cbb07d3fa925b2b7e5f8db5e1e53c /src/app.js | |
| parent | 64f96e031e6ea219cc03eb8aa2cc4d0cf7e1f4e4 (diff) | |
| download | coinosv3-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.js | 20 |
1 files changed, 20 insertions, 0 deletions
@@ -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', |
