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 = '

Not a valid calendar event.

' 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 = `

${title}

${timeStr}

${evt.content}

` // 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 = `

RSVP sent: ${status}

` } } catch (error) { console.error('failed to send RSVP:', error) const buttonsDiv = this.root.querySelector('[part="buttons"]') if (buttonsDiv) { buttonsDiv.innerHTML = `

failed to send RSVP: ${error}

` } } } } window.customElements.define('nostr-rsvp', NostrRSVP)