import { verifyEvent, NostrEvent } from '@nostr/tools/pure' import { decodeNostrURI, EventPointer, neventEncode, npubEncode, ProfilePointer } from '@nostr/tools/nip19' import { normalizeURL } from '@nostr/tools/utils' import { NostrUser } from '@nostr/gadgets/metadata' import * as nip10 from '@nostr/tools/nip10' import { fetchNostrUser, getOutboxRelaysFor, pool } from './nostr.js' import { debounce, nostrLink, shorten, splitComma } from './utils.js' import { renderText } from './text.js' export default class NostrNote extends HTMLElement { static observedAttributes = ['ref', 'event', 'relays', 'template'] root: HTMLDivElement setSeq = 0 constructor() { super() this.attachShadow({ mode: 'open' }) const { shadowRoot } = this this.root = document.createElement('div') shadowRoot!.appendChild(this.root) } connectedCallback() { setTimeout(() => { let template = this.getAttribute('template') ? (document.getElementById(this.getAttribute('template')!) as HTMLTemplateElement) : null if (template) { this.root.appendChild(template.content.cloneNode(true)) } else { this.root.innerHTML = `
parent
` } this.set() }, 1) } *queryPart(name: string): Generator { let slotted = this.querySelectorAll(`[part="${name}"]`) for (let i = 0; i < slotted.length; i++) { yield slotted[i] as E } let templated = this.root.querySelectorAll(`[part="${name}"]`) for (let i = 0; i < templated.length; i++) { yield templated[i] as E } } attributeChangedCallback() { this.set() } set: () => void = debounce(async () => { let seq = ++this.setSeq let evt: NostrEvent | null = null let hints: string[] = [] // if the raw event was provided as json, use that let eventj = this.getAttribute('event') if (eventj) { try { evt = JSON.parse(eventj) if (evt && !verifyEvent(evt)) return } catch (_err) { evt = null } } if (!evt) { // otherwise try to fetch from ref let ref = this.getAttribute('ref') if (ref) { let { type, data } = decodeNostrURI(ref) if (type !== 'nevent') { return } let d = data as EventPointer hints = (d.relays || []).concat(splitComma(this.getAttribute('relays')).map(normalizeURL)) let filter = { ids: [d.id] } // try to fetch from relay hints, falling back to the author's outbox relays let authorRelays: string[] = [] if (d.author) { authorRelays = await getOutboxRelaysFor(d.author) hints.push(...authorRelays) } evt = await pool.get(hints.length ? hints : authorRelays, filter) if (!evt && d.author && hints.length && seq === this.setSeq) { evt = await pool.get(authorRelays, filter) } } } if (seq !== this.setSeq || !evt) { return } let npub = npubEncode(evt.pubkey) for (let el of this.queryPart('author-link')) { el.href = nostrLink(npub) el.target = '_blank' } let nu: NostrUser | null = null for (let el of this.queryPart('author-name')) { if (!nu) { nu = await fetchNostrUser(evt.pubkey, hints) if (seq !== this.setSeq) return } el.textContent = nu.shortName } for (let el of this.queryPart('author-npub-short')) { el.textContent = shorten(npub) } let thread: { root: EventPointer | undefined reply: EventPointer | undefined mentions: EventPointer[] quotes: EventPointer[] profiles: ProfilePointer[] } | null = null for (let el of this.queryPart('parent-link')) { if (!thread) thread = nip10.parse(evt) if (thread.reply) { el.href = nostrLink(neventEncode(thread.reply)) el.target = '_blank' el.style.display = '' } else { el.remove() } } for (let el of this.queryPart('root-link')) { if (!thread) thread = nip10.parse(evt) if (thread.root) { el.href = nostrLink(neventEncode(thread.root)) el.target = '_blank' } else { el.remove() } } for (let el of this.queryPart('link')) { el.href = nostrLink( neventEncode({ id: evt.id, relays: Array.from(pool.seenOn.get(evt.id) || []) .slice(0, 3) .map(r => r.url), author: evt.pubkey, }), ) el.target = '_blank' } for (let el of this.queryPart('date')) { let date = new Date(evt.created_at * 1000) el.textContent = date.toLocaleString() if (el.tagName === 'TIME') { el.setAttribute('time', date.toISOString().substring(0, 23)) } } let contentWrapper: HTMLDivElement | null = null for (let el of this.queryPart('content')) { if (!contentWrapper) { contentWrapper = document.createElement('div') el.appendChild(contentWrapper) } renderText(contentWrapper, evt.content) } }, 200) } window.customElements.define('nostr-note', NostrNote)