diff --git a/assets/controllers/editor/layout_controller.js b/assets/controllers/editor/layout_controller.js
index 20f47a7..7ea993a 100644
--- a/assets/controllers/editor/layout_controller.js
+++ b/assets/controllers/editor/layout_controller.js
@@ -14,6 +14,9 @@ export default class extends Controller {
console.log('Editor layout controller connected');
this.autoSaveTimer = null;
+ // Expose method globally so login controllers can save state before reload
+ window.saveEditorStateBeforeLogin = () => this.saveCompleteStateBeforeLogin();
+
// --- Editor State Object ---
// See documentation/Editor/Reactivity-and-state-management.md
this.state = {
@@ -23,6 +26,10 @@ export default class extends Controller {
content_event_json: {} // Derived event JSON
};
this.hydrateState();
+
+ // Check for and restore state after login
+ this.restoreStateAfterLogin();
+
this.updateMarkdownEditor();
this.updateQuillEditor();
@@ -114,6 +121,87 @@ export default class extends Controller {
if (nmdField) nmdField.value = this.state.content_NMD || '';
}
+ saveCompleteStateBeforeLogin() {
+ // Save all editor content and form fields before login/reload
+ console.log('[Editor] Saving complete editor state before login');
+
+ // Sync current content first
+ this.syncContentBeforePublish();
+
+ const stateToSave = {
+ timestamp: Date.now(),
+ editorState: this.state,
+ formData: {}
+ };
+
+ // Save all form fields
+ const form = this.element.querySelector('form');
+ if (form) {
+ const formData = new FormData(form);
+ for (const [key, value] of formData.entries()) {
+ stateToSave.formData[key] = value;
+ }
+ }
+
+ localStorage.setItem('editorStateBeforeLogin', JSON.stringify(stateToSave));
+ console.log('[Editor] State saved:', stateToSave);
+ }
+
+ restoreStateAfterLogin() {
+ // Check if there's a saved state from before login
+ const savedStateStr = localStorage.getItem('editorStateBeforeLogin');
+ if (!savedStateStr) {
+ console.log('[Editor] No saved state found');
+ return;
+ }
+
+ try {
+ const savedState = JSON.parse(savedStateStr);
+ const ageMinutes = (Date.now() - savedState.timestamp) / 1000 / 60;
+
+ // Only restore if saved within last 10 minutes
+ if (ageMinutes > 10) {
+ console.log('[Editor] Saved state too old, ignoring');
+ localStorage.removeItem('editorStateBeforeLogin');
+ return;
+ }
+
+ console.log('[Editor] Restoring state from before login:', savedState);
+
+ // Restore editor state
+ if (savedState.editorState) {
+ this.state = savedState.editorState;
+ }
+
+ // Restore form fields
+ if (savedState.formData) {
+ const form = this.element.querySelector('form');
+ if (form) {
+ for (const [key, value] of Object.entries(savedState.formData)) {
+ const field = form.elements[key];
+ if (field) {
+ if (field.type === 'checkbox') {
+ field.checked = value === 'on' || value === '1' || value === true;
+ } else {
+ field.value = value;
+ }
+ }
+ }
+ }
+ }
+
+ // Clear the saved state
+ localStorage.removeItem('editorStateBeforeLogin');
+
+ // Show notification
+ this.updateStatus('✓ Your work has been restored after login');
+
+ } catch (e) {
+ console.error('[Editor] Failed to restore state:', e);
+ localStorage.removeItem('editorStateBeforeLogin');
+ }
+ }
+
// --- Tab Switching Logic ---
switchMode(event) {
const mode = event.currentTarget.dataset.mode;
diff --git a/assets/controllers/utility/login_controller.js b/assets/controllers/utility/login_controller.js
index a7f4568..07ae18d 100644
--- a/assets/controllers/utility/login_controller.js
+++ b/assets/controllers/utility/login_controller.js
@@ -1,12 +1,8 @@
import { Controller } from '@hotwired/stimulus';
-import { getComponent } from '@symfony/ux-live-component';
export default class extends Controller {
static targets = ['nostrError'];
- async initialize() {
- this.component = await getComponent(this.element);
- }
async loginAct(event) {
if (!window.nostr) {
if (this.hasNostrErrorTarget) {
@@ -45,8 +41,13 @@ export default class extends Controller {
if (!response.ok) return false;
return 'Authentication Successful';
})
+
if (!!result) {
- this.component.render();
+ // Save editor state before reload (if in editor)
+ if (typeof window.saveEditorStateBeforeLogin === 'function') {
+ window.saveEditorStateBeforeLogin();
+ }
+ window.location.reload();
}
}
}
diff --git a/assets/controllers/utility/signer_modal_controller.js b/assets/controllers/utility/signer_modal_controller.js
new file mode 100644
index 0000000..787266d
--- /dev/null
+++ b/assets/controllers/utility/signer_modal_controller.js
@@ -0,0 +1,148 @@
+import { Controller } from '@hotwired/stimulus';
+import { getPublicKey, SimplePool } from 'nostr-tools';
+import { BunkerSigner } from "nostr-tools/nip46";
+import { setRemoteSignerSession } from '../nostr/signer_manager.js';
+
+export default class extends Controller {
+ static targets = ['dialog', 'qr', 'status'];
+
+ connect() {
+ console.log('[signer-modal] controller connected');
+ this._localSecretKey = null;
+ this._uri = null;
+ this._relays = [];
+ this._secret = null;
+ this._signer = null;
+ this._pool = null;
+ this._didAuth = false;
+ }
+
+ disconnect() {
+ try { this._signer?.close?.(); } catch (_) {}
+ try { this._pool?.close?.([]); } catch (_) {}
+ }
+
+ async openDialog() {
+ console.log('[signer-modal] openDialog called', this.hasDialogTarget);
+ if (this.hasDialogTarget) {
+ this.dialogTarget.style.display = 'block';
+ await this._init();
+ } else {
+ console.error('[signer-modal] dialog target not found');
+ }
+ }
+
+ closeDialog() {
+ if (this.hasDialogTarget) {
+ this.dialogTarget.style.display = 'none';
+ // Clean up resources
+ try { this._signer?.close?.(); } catch (_) {}
+ try { this._pool?.close?.([]); } catch (_) {}
+ this._didAuth = false;
+ }
+ }
+
+ async _init() {
+ try {
+ this._setStatus('Requesting pairing QR…');
+ const res = await fetch('/nostr-connect/qr');
+ if (!res.ok) throw new Error('QR fetch failed');
+ const data = await res.json();
+
+ this._localSecretKey = data.privkey;
+ this._uri = data.uri;
+ this._relays = data.relays || [data.relay].filter(Boolean);
+ this._secret = data.secret || null;
+
+ if (this.hasQrTarget) {
+ this.qrTarget.innerHTML = ``;
+ }
+
+ this._checkClientPubkeyIntegrity();
+
+ this._setStatus('Scan with Amber (NIP-46)…');
+ await this._createSigner();
+ this._setStatus('Remote signer connected. Authenticating…');
+ await this._attemptAuth();
+ } catch (e) {
+ console.error('[signer-modal] init error', e);
+ this._setStatus('Init failed: ' + (e.message || 'unknown'));
+ }
+ }
+
+ _checkClientPubkeyIntegrity() {
+ try {
+ if (!this._localSecretKey || !this._uri) return;
+ const derived = getPublicKey(this._localSecretKey);
+ const m = this._uri.match(/^nostrconnect:\/\/([0-9a-fA-F]{64})/);
+ if (!m) {
+ console.warn('[signer-modal] URI missing/invalid pubkey segment');
+ return;
+ }
+ const uriPk = m[1].toLowerCase();
+ if (uriPk !== derived.toLowerCase()) {
+ console.warn('[signer-modal] Pubkey mismatch: derived != URI', { derived, uriPk });
+ }
+ } catch (e) {
+ console.warn('[signer-modal] integrity check failed', e);
+ }
+ }
+
+ async _createSigner() {
+ this._pool = new SimplePool();
+ this._setStatus('Waiting for remote signer…');
+ this._signer = await BunkerSigner.fromURI(this._localSecretKey, this._uri, { pool: this._pool });
+ }
+
+ async _attemptAuth() {
+ if (this._didAuth) return;
+ this._didAuth = true;
+ try {
+ const loginEvent = {
+ kind: 27235,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['u', window.location.origin + '/login'],
+ ['method', 'POST'],
+ ['t', 'bunker']
+ ],
+ content: ''
+ };
+ this._setStatus('Signing login request…');
+ const signed = await this._signer.signEvent(loginEvent);
+ this._setStatus('Submitting login…');
+ const resp = await fetch('/login', {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { 'Authorization': 'Nostr ' + btoa(JSON.stringify(signed)) }
+ });
+ if (resp.ok) {
+ setRemoteSignerSession({
+ privkey: this._localSecretKey,
+ bunkerPointer: this._signer.bp,
+ uri: this._uri,
+ relays: this._relays,
+ secret: this._secret
+ });
+ this._setStatus('Authenticated. Reloading…');
+
+ // Save editor state before reload (if in editor)
+ if (typeof window.saveEditorStateBeforeLogin === 'function') {
+ window.saveEditorStateBeforeLogin();
+ }
+
+ setTimeout(() => window.location.reload(), 500);
+ } else {
+ this._setStatus('Login failed (' + resp.status + ')');
+ }
+ } catch (e) {
+ console.error('[signer-modal] auth error', e);
+ this._setStatus('Auth error: ' + (e.message || 'unknown'));
+ }
+ }
+
+ _setStatus(msg) {
+ if (this.hasStatusTarget) this.statusTarget.textContent = msg;
+ }
+}
+
diff --git a/assets/styles/02-layout/layout.css b/assets/styles/02-layout/layout.css
index fa0105d..7a5911a 100644
--- a/assets/styles/02-layout/layout.css
+++ b/assets/styles/02-layout/layout.css
@@ -284,7 +284,7 @@ p.measure {
margin: 1em auto;
}
-section{ position: relative; padding: var(--section-spacing) var(--spacing-3); }
+section{ padding: var(--section-spacing) var(--spacing-3); }
.katex-display>.katex {
line-height: 2;
diff --git a/assets/styles/editor-layout.css b/assets/styles/editor-layout.css
index 5b1d027..67b0b30 100644
--- a/assets/styles/editor-layout.css
+++ b/assets/styles/editor-layout.css
@@ -448,10 +448,7 @@ main.editor-layout {
}
.articlelist-placeholder {
- color: #bbb;
- font-style: italic;
padding: 1.5rem 0;
- text-align: center;
}
/* Filesystem-style reading list folders */
diff --git a/src/Twig/Components/Footer.php b/src/Twig/Components/Footer.php
index 58bd34d..dcfadd0 100644
--- a/src/Twig/Components/Footer.php
+++ b/src/Twig/Components/Footer.php
@@ -8,7 +8,4 @@ use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Footer {
- public function __construct()
- {
- }
}
diff --git a/src/Twig/Components/Header.php b/src/Twig/Components/Header.php
index 857134b..1e345ea 100644
--- a/src/Twig/Components/Header.php
+++ b/src/Twig/Components/Header.php
@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Twig\Components;
-use Psr\Cache\InvalidArgumentException;
-use Symfony\Contracts\Cache\CacheInterface;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
diff --git a/src/Twig/Components/UserMenu.php b/src/Twig/Components/UserMenu.php
index 5596c93..226d3d2 100644
--- a/src/Twig/Components/UserMenu.php
+++ b/src/Twig/Components/UserMenu.php
@@ -1,12 +1,9 @@
+
Scan the QR below with a NIP-46 compatible bunker signer to pair a remote signing session.
+After pairing, any page that uses window.nostr will automatically use this remote signer session.
+If pairing stalls, close and reopen this dialog to generate a new ephemeral key.
+Use your Nostr credentials to authenticate.
- Login with a remote signer +