From da6c8360726f499aa5cccec0b76839fd868fdc0e Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 1 Oct 2026 23:02:13 -0700 Subject: Search finds people by their full name, spaces and all searchable() only let [a-z0-9._-] through, so "jordan miller" never searched. Words and non-Latin letters pass now, and a multi-word query also asks Primal with the spaces removed, since it matches "jordanmiller" but not "jordan miller". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo --- src/recipient-search.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/recipient-search.js b/src/recipient-search.js index 8d1ae84..b0a0edc 100644 --- a/src/recipient-search.js +++ b/src/recipient-search.js @@ -144,7 +144,8 @@ export function searchable(qRaw) { if (q.length < 2 || q.length > 30) return false; if (/^(bc1|tb1|bcrt1|lnbc|lntb|lnurl|bitcoin:|lightning:|sp1q|ark1|xpub|zpub|xprv|zprv|nsec1|bunker:)/.test(q)) return false; if (/^[0-9a-f]{40,}$/.test(q)) return false; // raw hex key/txid - return /^[a-z0-9._-]+$/.test(q); + // a display name is words: "jordan miller", "José", not just a handle + return /^[\p{L}\p{N}._-]+(?:\s+[\p{L}\p{N}._-]+)*$/u.test(q); } async function fillProfiles(rows) { @@ -171,7 +172,7 @@ async function fillProfiles(rows) { // waiting for the slow ones. The returned promise still resolves with the // final, profile-filled rows. export async function searchRecipients(qRaw, onPartial = null) { - const q = qRaw.trim().toLowerCase(); + const q = qRaw.trim().toLowerCase().replace(/\s+/g, ' '); if (queryCache.has(q)) return queryCache.get(q); const out = new Map(); // pk -> { pk, name, address, picture, pri } const add = (pk, row, pri) => { @@ -183,7 +184,7 @@ export async function searchRecipients(qRaw, onPartial = null) { // Within a source, a name that actually contains the query outranks the // search relay's looser relevance matches. - const score = (r) => ((r.name || '').toLowerCase().includes(q) || (r.address || r.nip05 || '').toLowerCase().includes(q)) ? 0 : 1; + const score = (r) => ((r.name || '').toLowerCase().replace(/\s+/g, ' ').includes(q) || (r.address || r.nip05 || '').toLowerCase().includes(q)) ? 0 : 1; const snapshot = () => { const rows = [...out.values()].sort((a, b) => a.pri - b.pri || score(a) - score(b)).slice(0, MAX_RESULTS); for (const r of rows) { @@ -210,7 +211,9 @@ export async function searchRecipients(qRaw, onPartial = null) { add(e.pubkey, { name: m.display_name || m.name, picture: m.picture, nip05: typeof m.nip05 === 'string' ? m.nip05.replace(/^_@/, '') : undefined }, pri); } catch {} }; - const primal = await primalSearch(q); + // Primal matches "jordanmiller" but not "jordan miller": ask both ways + const joined = q.replace(/ /g, ''); + const primal = (await Promise.all(joined === q ? [primalSearch(q)] : [primalSearch(q), primalSearch(joined)])).flat(); for (const e of primal) noteKind0(e, 2); if (!primal.length) { const evs = await queryOn(SEARCH_RELAYS, { kinds: [0], search: q, limit: MAX_RESULTS }, 3500); @@ -240,7 +243,7 @@ export function makeSearcher(onUpdate) { clearTimeout(timer); const my = ++seq; if (!searchable(q)) { onUpdate(q, null); return; } - const s = q.trim().toLowerCase(); + const s = q.trim().toLowerCase().replace(/\s+/g, ' '); // Get something on screen fast, then relax: the first short query fires // almost immediately (that's the wait the user actually feels), while // longer ones — mid-word keystrokes — wait long enough to skip the -- cgit v1.2.3