diff options
| -rw-r--r-- | src/features/messages.js | 51 | ||||
| -rw-r--r-- | tools/feed-custom-test.js | 14 |
2 files changed, 49 insertions, 16 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index d4b6b00..10828d9 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -3625,13 +3625,39 @@ export function messagesFeature(ctx) { return packMembers; }; const packOf = (p) => packsNow()[packKey(p)] || null; - // A pasted following.space link, a bare naddr, or nostr:naddr — the pack it names. + // A pasted following.space link (/d/<id>?p=<pubkey>), a bare naddr, or + // nostr:naddr — the pack it names. function parsePackLink(input) { - const m = /naddr1[a-z0-9]+/i.exec(String(input || '')); - if (!m) return null; - const ref = parseNostrRef(m[0].toLowerCase()); - if (!ref || ref.type !== 'addr' || ref.kind !== PACK_KIND) return null; - return { pk: ref.pk, d: ref.d, relays: ref.relays || [] }; + const s = String(input || '').trim(); + const m = /naddr1[a-z0-9]+/i.exec(s); + if (m) { + const ref = parseNostrRef(m[0].toLowerCase()); + return ref && ref.type === 'addr' && ref.kind === PACK_KIND ? { pk: ref.pk, d: ref.d, relays: ref.relays || [] } : null; + } + const link = /\/d\/([^/?#\s]+)[^?\s]*\?(?:[^#\s]*&)?p=([0-9a-f]{64})/i.exec(s); + if (link) return { pk: link[2].toLowerCase(), d: decodeURIComponent(link[1]), relays: [] }; + return null; + } + // Every pack the wide relays hold, once per session: the way to find one + // by a few letters of its title — NIP-50 search only knows the packs the + // search relay happens to carry, and most live on damus / nos.lol. + let packIndex = null; + function loadPackIndex() { + if (packIndex) return packIndex; + packIndex = (async () => { + const relays = [...new Set([...PACK_RELAYS, 'wss://relay.damus.io', 'wss://nos.lol'])]; + const all = await Promise.all(relays.map((r) => queryOn([r], { kinds: [PACK_KIND], limit: 500 }, 6000).catch(() => []))); + const newest = new Map(); + for (const ev of all.flat()) { + const p = packFromEvent(ev); + if (!p.pks.length) continue; + const cur = newest.get(packKey(p)); + if (!cur || p.at > cur.at) newest.set(packKey(p), { ...p, desc: (ev.tags.find((x) => x[0] === 'description') || [])[1] || '' }); + } + return [...newest.values()]; + })(); + packIndex.catch(() => { packIndex = null; }); + return packIndex; } const packFromEvent = (ev) => ({ pk: ev.pubkey, d: (ev.tags.find((x) => x[0] === 'd') || [])[1] || '', @@ -5935,16 +5961,15 @@ export function messagesFeature(ctx) { const got = await fetchPack(link).catch(() => null); rows = got ? [{ ...link, title: got.title, n: got.pks.length }] : []; } else { - const evs = await queryOn(['wss://relay.nostr.band'], { kinds: [PACK_KIND], search: q, limit: 12 }, 4000).catch(() => []); - const seen = new Set(); - for (const ev of (evs || []).sort((a, b) => b.created_at - a.created_at)) { - const p = packFromEvent(ev); - if (!p.pks.length || seen.has(packKey(p))) continue; - seen.add(packKey(p)); + const words = q.toLowerCase().split(/\s+/).filter(Boolean); + const index = await loadPackIndex().catch(() => []); + const hits = index.filter((p) => { const hay = (p.title + ' ' + p.desc).toLowerCase(); return words.every((w) => hay.includes(w)); }) + .sort((a, b) => b.pks.length - a.pks.length).slice(0, 12); + for (const p of hits) { packsNow()[packKey(p)] ||= { pks: p.pks, title: p.title, at: p.at }; rows.push({ pk: p.pk, d: p.d, relays: [], title: p.title, n: p.pks.length }); } - try { wallet.saveFeatureState(PACK_CACHE, packsNow()); } catch {} + if (hits.length) { try { wallet.saveFeatureState(PACK_CACHE, packsNow()); } catch {} } } if (seq !== packSearchSeq || !ui.feedEdit) return; ui.feedEdit.packRows = rows; diff --git a/tools/feed-custom-test.js b/tools/feed-custom-test.js index 8062a15..0fc7ab8 100644 --- a/tools/feed-custom-test.js +++ b/tools/feed-custom-test.js @@ -23,7 +23,8 @@ const seeded = [signed('hello from the feed, about #bitcoin today', 300), signed const CURATOR_SK = generateSecretKey(); const MEMBER = getPublicKey(generateSecretKey()); const PACK = finalizeEvent({ kind: 39089, created_at: Math.floor(Date.now() / 1000) - 100, tags: [['d', 'test-pack'], ['title', 'Test pack'], ['p', MEMBER]], content: '' }, CURATOR_SK); -const PACK_LINK = 'https://following.space/d/' + naddrEncode({ kind: 39089, pubkey: PACK.pubkey, identifier: 'test-pack', relays: [] }); +const PACK_LINK = 'https://following.space/d/test-pack?p=' + PACK.pubkey; // the form following.space shares +const PACK_NADDR = naddrEncode({ kind: 39089, pubkey: PACK.pubkey, identifier: 'test-pack', relays: [] }); const html = await buildHtml({ minify: true, pwa: false }); const server = Bun.serve({ port: 5297, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); @@ -130,9 +131,16 @@ try { console.log('\n[a follow pack from following.space]'); await page.click('.feed-chip.add'); await sleep(300); await type('input[placeholder="e.g. Bitcoin builders"]', 'Pack feed'); + await type('input[placeholder="Search packs, or paste a following.space link…"]', 'tes'); + check('a few letters of the title find the pack', await waitText('test pack', 8000) && await waitText('1 members', 2000)); + await type('input[placeholder="Search packs, or paste a following.space link…"]', 'zzzz'); + await sleep(900); + check('...and letters that match nothing find nothing', !(await page.evaluate(() => document.body.innerText)).includes('Test pack')); + await type('input[placeholder="Search packs, or paste a following.space link…"]', PACK_NADDR); + check('a bare naddr finds it', await waitText('test pack', 6000)); await type('input[placeholder="Search packs, or paste a following.space link…"]', PACK_LINK); - const found = await waitText('test pack', 6000); - check('a pasted following.space link finds the pack', found && await waitText('1 members', 2000)); + await sleep(900); + check('a pasted following.space link finds it', await waitText('test pack', 6000)); await page.evaluate(() => [...document.querySelectorAll('.row')].find((r) => /Test pack/.test(r.textContent) && !r.querySelector('button')).click()); await sleep(300); check('...and adds it to the feed', await page.evaluate(() => [...document.querySelectorAll('.row')].some((r) => /Test pack/.test(r.textContent) && r.querySelector('button')))); await page.evaluate(() => { window.__reqs = []; }); |
