nostr-web-components
Version:
collection of web components that provide quick access to basic nostr things
138 lines (114 loc) • 4.17 kB
text/typescript
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<H = HTMLElement>(name: string): Iterable<H> {
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)