diff --git a/assets/controllers/editor/header_controller.js b/assets/controllers/editor/header_controller.js index 878b724..a786ac8 100644 --- a/assets/controllers/editor/header_controller.js +++ b/assets/controllers/editor/header_controller.js @@ -35,6 +35,7 @@ export default class extends Controller { // Trigger click on the hidden Nostr publish button const publishButton = document.querySelector('[data-nostr--nostr-publish-target="publishButton"]'); if (publishButton) { + console.log('[Header] Triggering publish button click'); publishButton.click(); } else { console.error('[Header] Hidden publish button not found'); diff --git a/assets/controllers/editor/layout_controller.js b/assets/controllers/editor/layout_controller.js index dc9faf2..f1509f2 100644 --- a/assets/controllers/editor/layout_controller.js +++ b/assets/controllers/editor/layout_controller.js @@ -42,6 +42,34 @@ export default class extends Controller { this.element.addEventListener('content:changed', () => { this.updatePreview().then(r => console.log('Preview updated after content change', r)); }); + + this.setupAuthModal(); + } + + setupAuthModal() { + this.authModal = document.getElementById('auth-choice-modal'); + this.signerBtn = document.getElementById('proceed-with-signer'); + this.extensionBtn = document.getElementById('proceed-with-extension'); + this.isAnon = !window.appUser || !window.appUser.isAuthenticated; // You may need to set window.appUser in base.html.twig + if (this.signerBtn) { + this.signerBtn.addEventListener('click', () => { + this.hideAuthModal(); + this.startPublishWith('signer'); + }); + } + if (this.extensionBtn) { + this.extensionBtn.addEventListener('click', () => { + this.hideAuthModal(); + this.startPublishWith('extension'); + }); + } + } + + showAuthModal() { + if (this.authModal) this.authModal.style.display = 'block'; + } + hideAuthModal() { + if (this.authModal) this.authModal.style.display = 'none'; } hydrateState() { @@ -330,3 +358,4 @@ export default class extends Controller { })); } } + diff --git a/assets/controllers/nostr/nostr_publish_controller.js b/assets/controllers/nostr/nostr_publish_controller.js index 66a32f6..86b57f4 100644 --- a/assets/controllers/nostr/nostr_publish_controller.js +++ b/assets/controllers/nostr/nostr_publish_controller.js @@ -208,44 +208,43 @@ export default class extends Controller { event.preventDefault(); } + console.log('[nostr-publish] Publish triggered'); + if (!this.publishUrlValue) { this.showError('Publish URL is not configured'); return; } - if (!window.nostr) { - this.showError('Nostr extension not found'); - return; - } - this.publishButtonTarget.disabled = true; this.showStatus('Preparing article for signing...'); try { - // Use canonical CodeMirror JSON for publishing - let nostrEvent; - const jsonString = this.getCurrentJson(); - if (jsonString.trim()) { - try { - nostrEvent = JSON.parse(jsonString); - } catch (e) { - throw new Error('Invalid JSON in raw event area: ' + (e?.message || e)); - } + const formData = this.collectFormData(); + let nostrEvent = await this.createNostrEvent(formData); + + // Choose signing flow based on loginMethod + let signedEvent; + console.log('[nostr-publish] loginMethod:', formData.loginMethod); + if (formData.loginMethod === 'bunker') { + // Hand off to signer_manager via custom event + const handoffEvent = new CustomEvent('nostr:sign', { + detail: { nostrEvent, formData: formData }, + bubbles: true, + cancelable: true + }); + // Dispatch on the editor layout container or document + (this.element.closest('.editor-layout') || document).dispatchEvent(handoffEvent); + this.showStatus('Handed off to signer manager for signing.'); + this.publishButtonTarget.disabled = false; + return; } else { - // Fallback: regenerate from form data - nostrEvent = await this.createNostrEvent(this.collectFormData()); + // Get pubkey from extension and then signature + this.showStatus('Requesting pubkey from Nostr extension...'); + nostrEvent.pubkey = await window.nostr.getPublicKey(); + this.showStatus('Requesting signature from Nostr extension...'); + signedEvent = await window.nostr.signEvent(nostrEvent); } - // Ensure pubkey present before signing - if (!nostrEvent.pubkey) { - try { nostrEvent.pubkey = await window.nostr.getPublicKey(); } catch (_) {} - } - - this.showStatus('Requesting signature from Nostr extension...'); - - // Sign the event with Nostr extension - const signedEvent = await window.nostr.signEvent(nostrEvent); - this.showStatus('Publishing article...'); // Send to backend @@ -266,6 +265,12 @@ export default class extends Controller { } } + // Placeholder for the actual signer logic + async signWithSigner(event) { + // TODO: Implement the actual signer logic here + throw new Error('Signer signing flow is not yet implemented.'); + } + // If a user provided a partial or custom event, make sure required keys exist and supplement from form applyEventDefaults(event, formData, options = {}) { const now = Math.floor(Date.now() / 1000); @@ -335,13 +340,14 @@ export default class extends Controller { // Only use the Markdown field const content = fd.get('editor[content]') || ''; - const title = fd.get('editor[title]') || ''; const summary = fd.get('editor[summary]') || ''; const image = fd.get('editor[image]') || ''; const topicsString = fd.get('editor[topics]') || ''; const isDraft = fd.get('editor[isDraft]') === '1'; const addClientTag = fd.get('editor[clientTag]') === '1'; + const pubkey = fd.get('editor[pubkey]') || ''; + const loginMethod = fd.get('editor[loginMethod]') || ''; // Collect advanced metadata const advancedMetadata = this.collectAdvancedMetadata(fd); @@ -366,6 +372,8 @@ export default class extends Controller { isDraft, addClientTag, advancedMetadata, + pubkey, + loginMethod }; } @@ -412,18 +420,13 @@ export default class extends Controller { } async createNostrEvent(formData) { - // TODO This logic needs to be updated to take care of three distinct cases: - // 1. user not logged in: generate event from form data with placeholder pubkey - // 2. user logged in with extension: get pubkey from extension and generate event - // 3. user logged in with signer: get pubkey from signer and generate event - // ----------------------------------------------------------------------------- - // Get user's public key if available (preview can work without it) - let pubkey = ''; - try { - if (window.nostr && typeof window.nostr.getPublicKey === 'function') { - pubkey = await window.nostr.getPublicKey(); - } - } catch (_) {} + // Use pubkey and loginMethod from formData only + let pubkey = formData.pubkey; + + if (!pubkey) { + // Use placeholder + pubkey = ''; + } // Validate advanced metadata if present if (formData.advancedMetadata && formData.advancedMetadata.zapSplits.length > 0) { @@ -467,14 +470,13 @@ export default class extends Controller { const advancedTags = buildAdvancedTags(formData.advancedMetadata); tags.push(...advancedTags); } - - // Create the Nostr event (NIP-23 long-form content) + // Return the event object, with pubkey and loginMethod for signing logic return { kind: kind, created_at: Math.floor(Date.now() / 1000), tags: tags, content: formData.content, - pubkey: pubkey + pubkey: pubkey, }; } diff --git a/assets/controllers/nostr/nostr_single_sign_controller.js b/assets/controllers/nostr/nostr_single_sign_controller.js index e9ab61a..00a1b11 100644 --- a/assets/controllers/nostr/nostr_single_sign_controller.js +++ b/assets/controllers/nostr/nostr_single_sign_controller.js @@ -5,14 +5,44 @@ export default class extends Controller { static targets = ['status', 'publishButton', 'computedPreview']; static values = { event: String, - publishUrl: String, - csrfToken: String + publishUrl: String }; + // Make targets optional for event-driven usage + get hasStatusTarget() { + return this.targets.has('status'); + } + get hasPublishButtonTarget() { + return this.targets.has('publishButton'); + } + get hasComputedPreviewTarget() { + return this.targets.has('computedPreview'); + } + async connect() { try { await this.preparePreview(); } catch (_) {} + // Listen for nostr:sign event for editor handoff + this.handleSignEvent = this.handleSignEvent.bind(this); + (this.element.closest('.editor-layout') || document).addEventListener('nostr:sign', this.handleSignEvent); + } + + disconnect() { + (this.element.closest('.editor-layout') || document).removeEventListener('nostr:sign', this.handleSignEvent); + } + + handleSignEvent(e) { + const { nostrEvent, formData } = e.detail; + // Update eventValue for signing + this.showStatus('Event received for signing. Ready to sign and publish.'); + this.eventValue = JSON.stringify(nostrEvent); + // Store formData for later use in publishing + this.handoffFormData = formData; + // Trigger signing + this.signAndPublish(new Event('submit')).then(r => { + this.showStatus('Signing process completed.'); + }); } async preparePreview() { @@ -78,13 +108,15 @@ export default class extends Controller { return; } - if (!this.publishUrlValue || !this.csrfTokenValue) { - console.error('[nostr_single_sign] Missing config', { publishUrl: this.publishUrlValue, csrf: !!this.csrfTokenValue }); + if (!this.publishUrlValue) { + console.error('[nostr_single_sign] Missing config', { publishUrl: this.publishUrlValue}); this.showError('Missing config'); return; } - this.publishButtonTarget.disabled = true; + if (this.hasPublishButtonTarget) { + this.publishButtonTarget.disabled = true; + } try { this.showStatus('Preparing event...'); const pubkey = await signer.getPublicKey(); @@ -111,27 +143,36 @@ export default class extends Controller { this.showSuccess('Published successfully! Redirecting...'); - // Redirect to reading list index after successful publish - setTimeout(() => { - window.location.href = '/reading-list'; - }, 1500); + // Redirect to reading list index after successful publish (only for form-based usage) + if (this.hasPublishButtonTarget) { + setTimeout(() => { + window.location.href = '/reading-list'; + }, 1500); + } } catch (e) { console.error('[nostr_single_sign] Error during sign/publish:', e); this.showError(e.message || 'Publish failed'); } finally { - this.publishButtonTarget.disabled = false; + if (this.hasPublishButtonTarget) { + this.publishButtonTarget.disabled = false; + } } } async publishSigned(signedEvent) { + // Build request body - include formData if available (from editor handoff) + const body = { event: signedEvent }; + if (this.handoffFormData) { + body.formData = this.handoffFormData; + } + const res = await fetch(this.publishUrlValue, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': this.csrfTokenValue, 'X-Requested-With': 'XMLHttpRequest' }, - body: JSON.stringify({ event: signedEvent }) + body: JSON.stringify(body) }); if (!res.ok) { const data = await res.json().catch(() => ({})); diff --git a/src/Form/EditorType.php b/src/Form/EditorType.php index 24cb33c..eca37ae 100644 --- a/src/Form/EditorType.php +++ b/src/Form/EditorType.php @@ -79,7 +79,20 @@ class EditorType extends AbstractType 'required' => false, 'mapped' => false, 'attr' => ['type' => 'hidden'], - ]); + ]) + // user's pubkey + ->add('pubkey', HiddenType::class, [ + 'required' => false, + 'mapped' => false, + 'attr' => ['type' => 'hidden'], + ]) + // log in method, can be extension or bunker + ->add('loginMethod', HiddenType::class, [ + 'required' => false, + 'mapped' => false, + 'attr' => ['type' => 'hidden'], + ]) + ; // Apply the custom transformer $builder->get('topics') diff --git a/templates/editor/layout.html.twig b/templates/editor/layout.html.twig index 951c2c5..b1bbe8f 100644 --- a/templates/editor/layout.html.twig +++ b/templates/editor/layout.html.twig @@ -42,8 +42,11 @@ {% endblock %} {% block layout %} -
+
+
{# Insert the article list sidebar as the first grid column #} {% include 'editor/panels/_articlelist.html.twig' with { readingLists: readingLists is defined ? readingLists : [], @@ -269,6 +272,8 @@ + {{ form_row(form.pubkey, {'value': app.user ? app.user.userIdentifier|toHex : ''}) }} + {{ form_row(form.loginMethod, {'value': app.session.get('nostr_sign_method', '')}) }} {{ form_end(form) }}
@@ -285,6 +290,22 @@ data-action="nostr--nostr-publish#publish" > + + {# Modal for anon user authentication choice #} +
{% endblock %}