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 setSeq = 0 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 seq = ++this.setSeq 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 (/^[0-9a-f]{64}$/.test(input)) { filter = { ids: [input] } } else { return } let hints = relays.map(normalizeURL) if (author) { let authorRelays = await getOutboxRelaysFor(author) hints.push(...authorRelays) if (seq !== this.setSeq) return } let evt = await pool.get(hints, filter) if (seq !== this.setSeq) return if (!evt || (evt.kind !== 31922 && evt.kind !== 31923)) { this.root.innerHTML = '
Not a valid calendar event.
' return } this.eventData = evt const title = evt.tags.find((t: string[]) => t[0] === 'title')?.[1] || 'Untitled Event' let timeStr = '' if (evt.kind === 31922) { 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 { 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 = '' const container = document.createElement('div') container.setAttribute('part', 'container') const titleEl = document.createElement('p') titleEl.setAttribute('part', 'title') titleEl.title = identifier titleEl.textContent = title container.appendChild(titleEl) const timeEl = document.createElement('p') timeEl.setAttribute('part', 'time') timeEl.textContent = timeStr container.appendChild(timeEl) const descEl = document.createElement('p') descEl.setAttribute('part', 'description') descEl.textContent = evt.content container.appendChild(descEl) const buttonsDiv = document.createElement('div') buttonsDiv.setAttribute('part', 'buttons') const add = (part: string, label: string, slotName: string, status: 'accepted' | 'tentative' | 'declined') => { let button = document.createElement('button') button.setAttribute('part', part) const slot = document.createElement('slot') slot.name = slotName slot.textContent = label button.appendChild(slot) button.addEventListener('click', () => this.sendRSVP(status)) buttonsDiv.appendChild(button) } add('button accepted', 'Going', 'accepted', 'accepted') add('button tentative', 'Maybe', 'tentative', 'tentative') add('button declined', 'Not going', 'declined', 'declined') container.appendChild(buttonsDiv) this.root.appendChild(container) }, 200) async sendRSVP(status: 'accepted' | 'tentative' | 'declined') { if (!this.eventData) { console.error('Cannot RSVP: missing event data') return } const buttonsDiv = this.root.querySelector('[part="buttons"]') const showResult = (part: string, slotName: string, msg: string) => { if (!buttonsDiv) return buttonsDiv.innerHTML = '' const p = document.createElement('p') p.setAttribute('part', part) const slot = document.createElement('slot') slot.name = slotName slot.textContent = msg p.appendChild(slot) buttonsDiv.appendChild(p) } 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)], ['status', status], ['p', this.eventData.pubkey], ], }) const pubs = pool.publish(await getInboxRelaysFor(this.eventData!.pubkey), signedEvent) await Promise.any(pubs) showResult('rsvp-sent', 'rsvp-sent', `RSVP sent: ${status}`) } catch (error) { console.error('failed to send RSVP:', error) showResult('rsvp-failed', 'rsvp-failed', `failed to send RSVP: ${String(error)}`) } } } window.customElements.define('nostr-rsvp', NostrRSVP)