nostr-web-components
Version:
collection of web components that provide quick access to basic nostr things
168 lines (143 loc) • 5.27 kB
text/typescript
import { NostrEvent } from '@nostr/tools/pure'
import { decodeNostrURI } from '@nostr/tools/nip19'
import { normalizeURL } from '@nostr/tools/utils'
import { getInboxRelaysFor, getOutboxRelaysFor, getWindowNostr, pool } from './nostr.js'
import { debounce } from './utils.js'
export default class NostrRSVP extends HTMLElement {
static observedAttributes = ['ref']
root: HTMLDivElement
eventData: NostrEvent | null = null
constructor() {
super()
this.attachShadow({ mode: 'open' })
const { shadowRoot } = this
this.root = document.createElement('div')
shadowRoot!.appendChild(this.root)
}
connectedCallback() {
this.style.display = 'block'
this.style.width = 'fit-content'
this.style.height = 'fit-content'
this.set()
}
attributeChangedCallback() {
this.set()
}
set: () => void = debounce(async () => {
let input = this.getAttribute('ref')
if (!input) return
let { type, data } = decodeNostrURI(input)
let relays: string[] = []
let author: string | undefined
let filter: any
if (type === 'nevent') {
let d = data as any
relays = d.relays || []
author = d.author
filter = { ids: [d.id] }
} else if (type === 'naddr') {
let d = data as any
relays = d.relays || []
author = d.pubkey
filter = {
authors: [d.pubkey],
kinds: [d.kind],
'#d': [d.identifier],
}
} else if (input.match(/[0-9a-f]{64}/)) {
filter = { ids: [input] }
} else {
return
}
relays = relays.map(normalizeURL)
if (author) {
let authorRelays = await getOutboxRelaysFor(author)
relays.push(...authorRelays)
}
let evt = await pool.get(relays, filter)
if (!evt || (evt.kind !== 31922 && evt.kind !== 31923)) {
this.root.innerHTML = '<p part="error">Not a valid calendar event.</p>'
return
}
this.eventData = evt
// get title from tags
const title = evt.tags.find((t: string[]) => t[0] === 'title')?.[1] || 'Untitled Event'
// get start/end times or dates
let timeStr = ''
if (evt.kind === 31922) {
// date-based event
const start = evt.tags.find((t: string[]) => t[0] === 'start')?.[1]
const end = evt.tags.find((t: string[]) => t[0] === 'end')?.[1] || start
timeStr = `${start}${end !== start ? ` to ${end}` : ''}`
} else {
// time-based event
const start = evt.tags.find((t: string[]) => t[0] === 'start')?.[1]
const end = evt.tags.find((t: string[]) => t[0] === 'end')?.[1]
if (start) {
const startDate = new Date(parseInt(start) * 1000)
timeStr = startDate.toLocaleString()
if (end) {
const endDate = new Date(parseInt(end) * 1000)
timeStr += ` to ${endDate.toLocaleString()}`
}
}
}
const identifier = evt.tags.find((t: string[]) => t[0] === 'd')?.[1] || ''
this.root.innerHTML = `
<div part="container">
<p part="title" title="${identifier}">${title}</p>
<p part="time">${timeStr}</p>
<p part="description">${evt.content}</p>
<div part="buttons">
<button part="button accepted"><slot name="accepted">Going</slot></button>
<button part="button tentative"><slot name="tentative">Maybe</slot></button>
<button part="button declined"><slot name="declined">Not going</slot></button>
</div>
</div>
`
// add click handlers for RSVP buttons
const buttons = this.root.querySelectorAll('button')
buttons[0].addEventListener('click', () => this.sendRSVP('accepted'))
buttons[1].addEventListener('click', () => this.sendRSVP('tentative'))
buttons[2].addEventListener('click', () => this.sendRSVP('declined'))
}, 200)
async sendRSVP(status: 'accepted' | 'tentative' | 'declined') {
if (!this.eventData) {
console.error('Cannot RSVP: missing event data')
return
}
try {
const signedEvent = await (
await getWindowNostr()
).signEvent({
kind: 31925,
created_at: Math.floor(Date.now() / 1000),
content: '',
tags: [
['e', this.eventData.id],
[
'a',
`${this.eventData.kind}:${this.eventData.pubkey}:${this.eventData.tags.find((t: string[]) => t[0] === 'd')?.[1]}`,
],
['d', Math.random().toString().substring(8)], // this is retarded
['status', status],
['p', this.eventData.pubkey],
],
})
const pubs = pool.publish(await getInboxRelaysFor(this.eventData!.pubkey), signedEvent)
await Promise.all(pubs)
// update UI to show response was sent
const buttonsDiv = this.root.querySelector('[part="buttons"]')
if (buttonsDiv) {
buttonsDiv.innerHTML = `<p part="rsvp-sent"><slot name="rsvp-sent">RSVP sent: ${status}</slot></p>`
}
} catch (error) {
console.error('failed to send RSVP:', error)
const buttonsDiv = this.root.querySelector('[part="buttons"]')
if (buttonsDiv) {
buttonsDiv.innerHTML = `<p part="rsvp-failed"><slot name="rsvp-failed">failed to send RSVP: ${error}</slot></p>`
}
}
}
}
window.customElements.define('nostr-rsvp', NostrRSVP)