diff --git a/src/index.html b/src/index.html
index 6f7e2bc..8fecdab 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1542,6 +1542,10 @@
padding: 3px 8px; font-size: .8rem;
border: 1px solid var(--line); background: #fff; cursor: pointer;
}
+ .bookChapterEditorSourceBar button.primary {
+ background: var(--accent); color: #fff; border-color: var(--accent);
+ }
+ .bookChapterEditorSourceBar button:disabled { opacity: .55; cursor: progress; }
.bookChapterEditorSourceLabel {
font-size: .76rem; color: var(--muted); padding: 0 4px;
border: 1px dashed var(--line); border-radius: 4px;
@@ -4360,6 +4364,7 @@
Local only
+
@@ -9873,6 +9878,92 @@
if (pinBtn) pinBtn.disabled = false;
}
}
+ // Build a NIP-23 d-tag slug from the chapter title (or entry id).
+ // Kept lowercase alphanumeric + hyphens so it round-trips through
+ // relay query strings cleanly.
+ function _bookChapterDTag(entry) {
+ if (entry?.textRef?.dTag && entry.textRef.pubkey
+ && state.community?.myPubkey
+ && entry.textRef.pubkey === state.community.myPubkey) {
+ // Re-publishing our own article — reuse the d-tag so the new
+ // event supersedes the old one via NIP-33 replaceability.
+ return entry.textRef.dTag;
+ }
+ const slug = String(entry?.chapterTitle || '')
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ .slice(0, 64);
+ if (slug) return `chapter:${slug}`;
+ // Fall back to the entry id so we still get a stable d-tag for
+ // untitled chapters.
+ return `chapter:${(entry?.id || 'unknown').replace(/[^a-z0-9:-]/gi, '')}`;
+ }
+ // Publish the current chapter as a kind-30023 NIP-23 article and
+ // update the entry's textRef so subsequent reads pull from the
+ // user's own publish. Replaces existing self-authored articles
+ // in-place (NIP-33) by reusing the prior d-tag.
+ async function publishBookChapterToNostr() {
+ const book = state.book;
+ const id = book?.editingChapterId;
+ if (!id) return;
+ const entry = book.pages.find(p => p.id === id);
+ if (!entry || entry.type !== 'reflow') return;
+ const msg = document.getElementById('bookChapterEditorSourceMsg');
+ const setMsg = (text, kind) => {
+ if (!msg) return;
+ msg.textContent = text || '';
+ msg.className = 'bookChapterEditorSourceMsg' + (kind ? ' ' + kind : '');
+ };
+ if (!window.nostr) { setMsg('Sign in with a Nostr extension first.', 'error'); return; }
+ const md = String(entry.chapterMarkdown || '').trim();
+ if (!md) { setMsg('Chapter is empty — nothing to publish.', 'error'); return; }
+ const title = (entry.chapterTitle || '').trim();
+ if (!title) { setMsg('Give the chapter a title before publishing.', 'error'); return; }
+ // Pull pubkey up-front so we can decide whether to reuse the
+ // existing d-tag (own article) vs mint a fresh one (replacing a
+ // foreign reference with our own copy).
+ let myPubkey = state.community?.myPubkey || '';
+ if (!myPubkey) {
+ try { myPubkey = await window.nostr.getPublicKey(); } catch {}
+ }
+ if (!myPubkey) { setMsg('Could not read pubkey from Nostr extension.', 'error'); return; }
+ // Confirm before overwriting another author's reference with
+ // our own publish — easy to do by accident with an imported
+ // chapter.
+ const ref = entry.textRef;
+ if (ref && ref.pubkey && ref.pubkey !== myPubkey) {
+ const ok = confirm(`This chapter is linked to ${ref.pubkey.slice(0,8)}…'s article. Publishing will replace the link with your own copy under your pubkey. Continue?`);
+ if (!ok) { setMsg('Publish cancelled.', 'error'); return; }
+ }
+ const dTag = _bookChapterDTag(entry);
+ const now = Math.floor(Date.now() / 1000);
+ const tags = [
+ ['d', dTag],
+ ['title', title],
+ ['published_at', String(now)],
+ ['t', 'book-chapter'],
+ ];
+ const evt = { kind: 30023, created_at: now, tags, content: md };
+ const btn = document.getElementById('bookChapterEditorPublishBtn');
+ if (btn) btn.disabled = true;
+ setMsg('Signing + publishing…');
+ try {
+ const signed = await publishCommunityEvent(evt);
+ const relays = splitRelayList(state.community?.relays || '').slice(0, 2);
+ pushHistory('book: chapter publish');
+ entry.textRef = { kind: 30023, pubkey: signed.pubkey, dTag, relays };
+ entry.textPinnedEventId = null; // own publishes default to live
+ setMsg(`Published as ${signed.id.slice(0, 8)}… (${md.length.toLocaleString()} chars).`, 'ok');
+ syncBookChapterEditor();
+ render();
+ } catch (err) {
+ console.warn('chapter publish failed', err);
+ setMsg('Publish failed: ' + (err?.message || 'unknown error'), 'error');
+ } finally {
+ if (btn) btn.disabled = false;
+ }
+ }
function unlinkBookChapterFromNostr() {
const book = state.book;
const id = book?.editingChapterId;
@@ -33189,6 +33280,7 @@
on('bookChapterEditorTextarea', 'input', (e) => updateBookChapterMarkdown(e.target.value));
on('bookChapterEditorTitle', 'input', (e) => updateBookChapterTitle(e.target.value));
on('bookChapterEditorImportBtn', 'click', importBookChapterFromNostr);
+ on('bookChapterEditorPublishBtn', 'click', publishBookChapterToNostr);
on('bookChapterEditorRefreshBtn', 'click', refreshBookChapterFromNostr);
on('bookChapterEditorPinBtn', 'click', toggleBookChapterPin);
on('bookChapterEditorUnlinkBtn', 'click', unlinkBookChapterFromNostr);