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)