import { normalizeURL } from '@nostr/tools/utils' import { decode } from '@nostr/tools/nip19' import { nostrUserFromEvent } from '@nostr/gadgets/metadata' import { debounce, handleImageError, splitComma, transparentPixel } from './utils.js' import { pool } from './nostr.js' export default class NostrUserSearch extends HTMLElement { static observedAttributes = ['relays', 'limit', 'placeholder', 'value'] root: HTMLDivElement limit = 10 value: string | undefined relays: string[] = ['wss://nostr.wine', 'wss://search.nos.today', 'wss://relay.nostr.band'].map(normalizeURL) constructor() { super() this.attachShadow({ mode: 'open' }) const { shadowRoot } = this this.root = document.createElement('div') shadowRoot!.appendChild(this.root) } connectedCallback() { this.style.display = 'inline-block' this.style.width = 'fit-content' this.style.height = 'fit-content' setTimeout(() => { let template = this.getAttribute('template') ? (document.getElementById(this.getAttribute('template')!) as HTMLTemplateElement) : null if (template) { this.root.appendChild(template.content.cloneNode(true)) } else { this.root.innerHTML = `
` } for (let input of this.queryPart('input')) { // setup input listener input.oninput = ev => this.search((ev.target as HTMLInputElement).value.trim()) // apply any attributes that were set before the template was loaded if (this.hasAttribute('placeholder')) { input.setAttribute('placeholder', this.getAttribute('placeholder')!) } if (this.hasAttribute('value')) { input.value = this.getAttribute('value')! this.search(input.value.trim()) } } }, 1) } attributeChangedCallback(name: string, _old: string, value: string) { if (name === 'relays') { this.relays = splitComma(value).map(normalizeURL) } else if (name === 'limit') { this.limit = parseInt(value) || 10 } else if (name === 'placeholder') { for (let input of this.queryPart('input')) { input.setAttribute('placeholder', value) } } else if (name === 'value') { for (let input of this.queryPart('input')) { input.value = value } this.search(value.trim()) } } search: (_: string) => void = debounce(async (q: string) => { for (let results of this.queryPart('results')) { if (q.length < 2) { results.style.display = 'none' return } for (let i = 0; i < results.children.length; i++) { if (results.children[i].tagName === 'TEMPLATE') continue results.removeChild(results.children[i]) } results.style.display = 'block' } pool.subscribeManyEose( this.relays, { search: q, limit: this.limit, kinds: [0] }, { onevent: evt => { let nu = nostrUserFromEvent(evt) for (let results of this.queryPart('results')) { const template = results.querySelector('template') as HTMLTemplateElement if (!template) continue const item = template.content.cloneNode(true) as DocumentFragment const itemElement = item.querySelector('[part="item"]') as HTMLElement if (itemElement) { itemElement.title = nu.npub itemElement.onclick = this.handleItemClicked.bind(this) itemElement.dataset.metadata = evt.content // set picture const pic = item.querySelector('[part="picture"]') as HTMLImageElement if (pic) { pic.onerror = handleImageError pic.src = nu.image || transparentPixel } // set name const name = item.querySelector('[part="name"]') as HTMLElement if (name) { name.textContent = nu.shortName } // Set nip05 const nip05 = item.querySelector('[part="nip05"]') as HTMLElement if (nip05 && nu.metadata.nip05) { nip05.textContent = nu.metadata.nip05 } results.appendChild(item) } } }, }, ) }, 450) handleItemClicked(ev: MouseEvent) { let item = ev.currentTarget! as HTMLLIElement let npub = item.title let pubkey = decode(npub).data as string for (let results of this.queryPart('results')) { for (let i = 0; i < results.children.length; i++) { if (results.children[i].tagName === 'TEMPLATE') continue results.removeChild(results.children[i]) } results.style.display = 'none' } this.value = pubkey this.dispatchEvent( new CustomEvent('selected', { bubbles: true, detail: { pubkey, npub, metadata: JSON.parse(item.dataset.metadata!) }, }), ) } *queryPart(name: string): Iterable { let slotted = this.querySelectorAll(`[part="${name}"]`) for (let i = 0; i < slotted.length; i++) { yield slotted[i] as H } let templated = this.root.querySelectorAll(`[part="${name}"]`) for (let i = 0; i < templated.length; i++) { yield templated[i] as H } } } window.customElements.define('nostr-user-search', NostrUserSearch)