You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
189 lines
6.4 KiB
189 lines
6.4 KiB
import { Controller } from '@hotwired/stimulus'; |
|
|
|
/** |
|
* NIP-22 kind-1111 reply: optional collapsed panel (Reply button), sign with NIP-07, POST, refresh thread. |
|
*/ |
|
export default class extends Controller { |
|
static targets = ['hint', 'panel', 'toggleBtn']; |
|
|
|
static values = { |
|
publishUrl: String, |
|
csrf: String, |
|
expectedCoordinate: String, |
|
articleEventId: String, |
|
fragmentUrl: String, |
|
refreshAfter: { type: Boolean, default: true }, |
|
blurbLabel: String, |
|
expectedTags: Array, |
|
parentKind: Number, |
|
parentId: String, |
|
authorPubkey: String, |
|
}; |
|
|
|
connect() { |
|
this._tags = this.expectedTagsValue; |
|
if (!Array.isArray(this._tags)) { |
|
const raw = this.element.getAttribute('data-comment-reply-expected-tags-value'); |
|
try { |
|
this._tags = raw ? JSON.parse(raw) : []; |
|
} catch { |
|
this._tags = []; |
|
} |
|
} |
|
} |
|
|
|
togglePanel() { |
|
if (!this.hasPanelTarget) { |
|
return; |
|
} |
|
const hidden = this.panelTarget.classList.toggle('comment-reply__panel--hidden'); |
|
const open = !hidden; |
|
if (this.hasToggleBtnTarget) { |
|
this.toggleBtnTarget.setAttribute('aria-expanded', open ? 'true' : 'false'); |
|
} |
|
if (open) { |
|
const ta = this.panelTarget.querySelector('textarea[name="body"]'); |
|
requestAnimationFrame(() => ta?.focus()); |
|
} |
|
} |
|
|
|
/** |
|
* @param {Event} ev |
|
*/ |
|
async publish(ev) { |
|
ev.preventDefault(); |
|
if (!this.hasNip07()) { |
|
this.setHint('Install a Nostr extension (NIP-07) to sign comments.'); |
|
return; |
|
} |
|
const root = this.hasPanelTarget ? this.panelTarget : this.element; |
|
const ta = root.querySelector('textarea[name="body"]'); |
|
const text = (ta?.value ?? '').trim(); |
|
if (!text) { |
|
this.setHint('Write something first.'); |
|
return; |
|
} |
|
if (this._tags.length === 0) { |
|
this.setHint('Missing NIP-22 tag template.'); |
|
return; |
|
} |
|
this.setHint('Preparing event…'); |
|
const { nip19 } = await import('nostr-tools'); |
|
const link = this.buildParentBech32(nip19); |
|
const blurb = `> Replying to **${this.blurbLabelValue}** — [view parent](nostr:${link})\n\n`; |
|
const unsigned = { |
|
kind: 1111, |
|
created_at: Math.floor(Date.now() / 1000), |
|
tags: this._tags, |
|
content: blurb + text, |
|
}; |
|
let signed; |
|
try { |
|
signed = await window.nostr.signEvent(unsigned); |
|
} catch (err) { |
|
this.setHint(`Signing failed: ${err instanceof Error ? err.message : String(err)}`); |
|
return; |
|
} |
|
this.setHint('Publishing…'); |
|
const payload = { |
|
event: signed, |
|
expected_coordinate: this.expectedCoordinateValue, |
|
parent_kind: parseInt(String(this.parentKindValue), 10), |
|
parent_id: this.parentIdValue, |
|
parent_author_pubkey: this.authorPubkeyValue, |
|
article_event_id: this.articleEventIdValue || null, |
|
csrf: this.csrfValue, |
|
}; |
|
let res; |
|
try { |
|
res = await fetch(this.publishUrlValue, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
Accept: 'application/json', |
|
'X-CSRF-TOKEN': this.csrfValue, |
|
}, |
|
credentials: 'same-origin', |
|
body: JSON.stringify(payload), |
|
}); |
|
} catch (err) { |
|
this.setHint(`Network error: ${err instanceof Error ? err.message : String(err)}`); |
|
return; |
|
} |
|
const data = await res.json().catch(() => ({})); |
|
if (!res.ok) { |
|
this.setHint(data.error || `HTTP ${res.status}`); |
|
return; |
|
} |
|
this.setHint('Published.'); |
|
if (ta) { |
|
ta.value = ''; |
|
} |
|
if (this.hasPanelTarget) { |
|
this.panelTarget.classList.add('comment-reply__panel--hidden'); |
|
if (this.hasToggleBtnTarget) { |
|
this.toggleBtnTarget.setAttribute('aria-expanded', 'false'); |
|
} |
|
} |
|
if (this.refreshAfterValue && this.fragmentUrlValue) { |
|
this.refreshThread(); |
|
} |
|
} |
|
|
|
hasNip07() { |
|
return typeof window.nostr !== 'undefined' && typeof window.nostr.signEvent === 'function'; |
|
} |
|
|
|
/** |
|
* @param {import('nostr-tools').nip19} nip19 |
|
*/ |
|
buildParentBech32(nip19) { |
|
const allZero = /^0{64}$/.test(this.parentIdValue); |
|
const parts = (this.expectedCoordinateValue || '').split(':'); |
|
const k = parts[0] ? parseInt(parts[0], 10) : 30023; |
|
const pub = parts[1] || this.authorPubkeyValue; |
|
const d = parts[2] || ''; |
|
if (allZero && d !== '') { |
|
return nip19.naddrEncode({ kind: k, pubkey: pub, identifier: d, relays: [] }); |
|
} |
|
return nip19.neventEncode({ |
|
id: this.parentIdValue, |
|
kind: this.parentKindValue, |
|
pubkey: this.authorPubkeyValue, |
|
relays: [], |
|
}); |
|
} |
|
|
|
refreshThread() { |
|
const wrap = this.element.closest('[data-article-comments-wrapper]'); |
|
const url = |
|
wrap?.getAttribute('data-article-comments-url-value') || |
|
document.querySelector('[data-article-comments-wrapper]')?.getAttribute('data-article-comments-url-value'); |
|
const container = |
|
wrap?.querySelector('[data-article-comments-target="container"]') || |
|
document.querySelector('[data-article-comments-target="container"]'); |
|
if (!url || !container) { |
|
window.location.reload(); |
|
return; |
|
} |
|
const bust = `cb=${Date.now()}`; |
|
const u = url.includes('?') ? `${url}&${bust}` : `${url}?${bust}`; |
|
void fetch(u, { headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' } }) |
|
.then((r) => (r.ok ? r.text() : Promise.reject(new Error(String(r.status))))) |
|
.then((html) => { |
|
container.innerHTML = html; |
|
}) |
|
.catch(() => { |
|
window.location.reload(); |
|
}); |
|
} |
|
|
|
/** |
|
* @param {string} msg |
|
*/ |
|
setHint(msg) { |
|
if (this.hasHintTarget) { |
|
this.hintTarget.textContent = msg; |
|
} |
|
} |
|
}
|
|
|