import { getOutboxRelaysFor, getWindowNostr, inputToPubkey, pool } from './nostr.js' export default class NostrFollow extends HTMLElement { static observedAttributes = ['pubkey'] button: HTMLButtonElement slotDefault: HTMLSlotElement slotLoading: HTMLSlotElement slotSuccess: HTMLSlotElement slotFailure: HTMLSlotElement constructor() { super() this.attachShadow({ mode: 'open' }) const { shadowRoot } = this this.button = document.createElement('button') this.button.setAttribute('part', 'button') this.button.onclick = this.handleClick.bind(this) this.slotDefault = document.createElement('slot') this.slotDefault.textContent = 'Follow me on Nostr!' this.button.appendChild(this.slotDefault) this.slotLoading = document.createElement('slot') this.slotLoading.name = 'loading' this.slotLoading.style.display = 'none' this.slotLoading.textContent = 'Following...' this.button.appendChild(this.slotLoading) this.slotSuccess = document.createElement('slot') this.slotSuccess.name = 'success' this.slotSuccess.style.display = 'none' this.slotSuccess.textContent = 'Followed.' this.button.appendChild(this.slotSuccess) this.slotFailure = document.createElement('slot') this.slotFailure.name = 'failure' this.slotFailure.style.display = 'none' this.slotFailure.textContent = 'Error: ' this.button.appendChild(this.slotFailure) const errorMessage = document.createElement('span') errorMessage.setAttribute('part', 'error-message') this.slotFailure.appendChild(errorMessage) shadowRoot!.appendChild(this.button) } *queryPart(name: string): Iterable { let slotted = this.button.querySelectorAll(`[part="${name}"]`) for (let i = 0; i < slotted.length; i++) { yield slotted[i] as H } let slotted2 = this.querySelectorAll(`[part="${name}"]`) for (let i = 0; i < slotted2.length; i++) { yield slotted2[i] as H } } async handleClick() { const [target, hints] = await inputToPubkey(this.getAttribute('pubkey')!) if (!target) return this.button.onclick = null this.button.disabled = true this.slotLoading.style.display = '' this.slotDefault.style.display = 'none' try { // get current follows const publicKey = await (await getWindowNostr()).getPublicKey() const relays = await getOutboxRelaysFor(publicKey) relays.push(...hints) // fetch existing contact list const res = await pool.querySync(relays, { kinds: [3], authors: [publicKey], }) if (res.length === 0) { throw new Error("couldn't find your contact list") } const event = res[0] if (event.tags.find(tag => tag[1] === target)) { // already following this.slotLoading.style.display = 'none' this.slotSuccess.style.display = '' this.dispatchEvent( new CustomEvent('success', { bubbles: true, detail: { already: true }, }), ) return } event.tags.push(['p', target, relays[0]]) event.created_at = Math.round(Date.now() / 1000) const signedEvent = await (await getWindowNostr()).signEvent(event) await Promise.any(pool.publish(relays, signedEvent)) this.slotLoading.style.display = 'none' this.slotSuccess.style.display = '' this.dispatchEvent( new CustomEvent('success', { bubbles: true, detail: { already: false }, }), ) } catch (error) { console.warn('failed to follow:', error) this.slotLoading.style.display = 'none' this.slotFailure.style.display = '' this.dispatchEvent( new CustomEvent('failure', { bubbles: true, detail: { message: String(error) }, }), ) for (let el of this.queryPart('error-message')) { let msg = String(error) if (msg.startsWith('Error:')) { msg = msg.substring(6) } el.textContent = msg.trim() } } } } window.customElements.define('nostr-follow', NostrFollow)