Browse Source

Editor: revive publishing

imwald
Nuša Pukšič 1 week ago
parent
commit
ab4b265dd0
  1. 1
      assets/controllers/editor/header_controller.js
  2. 29
      assets/controllers/editor/layout_controller.js
  3. 86
      assets/controllers/nostr/nostr_publish_controller.js
  4. 65
      assets/controllers/nostr/nostr_single_sign_controller.js
  5. 15
      src/Form/EditorType.php
  6. 23
      templates/editor/layout.html.twig

1
assets/controllers/editor/header_controller.js

@ -35,6 +35,7 @@ export default class extends Controller { @@ -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');

29
assets/controllers/editor/layout_controller.js

@ -42,6 +42,34 @@ export default class extends Controller { @@ -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 { @@ -330,3 +358,4 @@ export default class extends Controller {
}));
}
}

86
assets/controllers/nostr/nostr_publish_controller.js

@ -208,44 +208,43 @@ export default class extends Controller { @@ -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 { @@ -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 { @@ -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 { @@ -366,6 +372,8 @@ export default class extends Controller {
isDraft,
addClientTag,
advancedMetadata,
pubkey,
loginMethod
};
}
@ -412,18 +420,13 @@ export default class extends Controller { @@ -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 = '<pubkey>';
}
// Validate advanced metadata if present
if (formData.advancedMetadata && formData.advancedMetadata.zapSplits.length > 0) {
@ -467,14 +470,13 @@ export default class extends Controller { @@ -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,
};
}

65
assets/controllers/nostr/nostr_single_sign_controller.js

@ -5,14 +5,44 @@ export default class extends Controller { @@ -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 { @@ -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 { @@ -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(() => ({}));

15
src/Form/EditorType.php

@ -79,7 +79,20 @@ class EditorType extends AbstractType @@ -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')

23
templates/editor/layout.html.twig

@ -42,8 +42,11 @@ @@ -42,8 +42,11 @@
{% endblock %}
{% block layout %}
<main data-controller="editor--layout" data-article-id="{{ article.id|default('') }}">
<main class="editor-layout" data-controller="editor--layout nostr--nostr-single-sign" data-article-id="{{ article.id|default('') }}" data-nostr--nostr-single-sign-publish-url-value="{{ path('api-article-publish') }}">
<div class="editor-main">
<div data-nostr--nostr-single-sign-target="status"
style="position: fixed;top: 65px;right: 10px;"
></div>
{# 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 @@ @@ -269,6 +272,8 @@
</section>
</aside>
{{ 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) }}
</div>
@ -285,6 +290,22 @@ @@ -285,6 +290,22 @@
data-action="nostr--nostr-publish#publish"
></button>
</div>
{# Modal for anon user authentication choice #}
<div id="auth-choice-modal" class="modal" tabindex="-1" style="display:none;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Choose Authentication Method</h5>
</div>
<div class="modal-body">
<p>You need to sign in to publish. Please choose a method:</p>
<button type="button" class="btn btn-accent" id="proceed-with-signer">Proceed with Signer</button>
<button type="button" class="btn btn-accent" id="proceed-with-extension">Proceed with Extension</button>
</div>
</div>
</div>
</div>
</main>
{% endblock %}

Loading…
Cancel
Save