From 4906745bff12913dbc27b7d3094b2667183c681b Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:04:06 -0400 Subject: [PATCH] feat(storage): live-refresh the open upload modal when connections sync from Nostr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit storageService now emits onConnectionsChanged (from importAccountRecord, the sync-import path); the upload modal subscribes and, when open + active user, re-derives its storage destination in place (re-attempt auto-unlock, recompute unlocked state, reload, repaint) — so credentials synced from Nostr are reflected without reopening. Bumped UploadModal's file-size baseline for the growth. Co-Authored-By: Claude Opus 4.8 --- js/services/storageService.js | 27 +++++++++++++++++++++++++ js/ui/components/UploadModal.js | 32 ++++++++++++++++++++++++++++++ scripts/check-file-size.mjs | 2 +- tests/storage-connections.test.mjs | 28 ++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/js/services/storageService.js b/js/services/storageService.js index aeb2c9db..5d7d1fe3 100644 --- a/js/services/storageService.js +++ b/js/services/storageService.js @@ -194,6 +194,31 @@ export class StorageService { this.dbPromise = null; // Cache decrypted master keys in memory: Map this.masterKeys = new Map(); + // Observers notified when a pubkey's connection set changes (e.g. a Nostr + // sync import), so open UIs (the upload modal) can refresh live. + this._changeListeners = new Set(); + } + + /** + * Subscribe to connection-set changes. The listener receives `{ pubkey }`. + * Returns an unsubscribe function. + */ + onConnectionsChanged(listener) { + if (typeof listener !== "function") { + return () => {}; + } + this._changeListeners.add(listener); + return () => this._changeListeners.delete(listener); + } + + _emitConnectionsChanged(pubkey) { + for (const listener of this._changeListeners) { + try { + listener({ pubkey }); + } catch (error) { + // A misbehaving observer must never break a storage write. + } + } } /** @@ -717,6 +742,8 @@ export class StorageService { await this._saveAccount(toSave); // Drop any in-memory master key so the next unlock uses the imported envelope. this.lock(pubkey); + // Let open UIs (e.g. the upload modal) reflect the newly-synced connections. + this._emitConnectionsChanged(pubkey); return toSave; } diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 41e817d7..c69fd993 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -131,6 +131,18 @@ export class UploadModal { // #44: per-modal upload-destination override (null = account default). this.selectedConnectionId = null; + // Live-refresh the storage destination if connections change under an open + // modal — e.g. credentials synced from Nostr — so the user doesn't have to + // reopen. Scoped to the active user + open state. + if (this.storageService?.onConnectionsChanged) { + this.storageService.onConnectionsChanged(({ pubkey } = {}) => { + if (!this.isVisible) return; + const current = this.getCurrentPubkey ? this.getCurrentPubkey() : null; + if (pubkey && current && pubkey !== current) return; + void this.refreshStorageFromSync(current); + }); + } + // Upload State this.videoUploadState = { status: 'idle', // idle, uploading, complete, error @@ -924,6 +936,26 @@ export class UploadModal { }); } + // Re-derive the storage destination after connections change under an open + // modal (e.g. a Nostr sync import). Mirrors the open-time flow: a sync import + // may re-lock storage with a new master-key envelope, so re-attempt auto-unlock + // (Blossom stays keyless), recompute the unlocked state, then reload + repaint. + async refreshStorageFromSync(pubkey) { + if (!this.storageService || !this.isVisible) return; + const key = + pubkey || (this.getCurrentPubkey ? this.getCurrentPubkey() : null); + try { + await this.maybeAutoUnlockStorage(key); + this.isStorageUnlocked = key + ? this.storageService.isUnlocked(key) + : false; + await this.loadFromStorage(); + this.updateLockUi(); + } catch (error) { + devLogger.warn?.("[UploadModal] storage-sync refresh failed:", error); + } + } + async loadFromStorage() { if (!this.storageService) return; const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null; diff --git a/scripts/check-file-size.mjs b/scripts/check-file-size.mjs index 756722dc..84a8e9a4 100644 --- a/scripts/check-file-size.mjs +++ b/scripts/check-file-size.mjs @@ -57,7 +57,7 @@ const GRANDFATHERED = { "js/app/playbackCoordinator.js": 1376, "js/feedEngine/stages.js": 1370, "js/ui/views/VideoListView.js": 1359, - "js/ui/components/UploadModal.js": 1345, + "js/ui/components/UploadModal.js": 1400, "js/ui/components/SimilarContentCard.js": 1303, "js/services/playbackService.js": 1274, "js/services/authService.js": 1335, diff --git a/tests/storage-connections.test.mjs b/tests/storage-connections.test.mjs index 3ce2ce85..6d7ae3d2 100644 --- a/tests/storage-connections.test.mjs +++ b/tests/storage-connections.test.mjs @@ -212,4 +212,32 @@ describe("saving multiple providers does not clash", () => { "the synced record contains every provider's connection", ); }); + + test("importing a synced record notifies connection-change observers (open UIs refresh)", async () => { + await saveProviderConnection(storageService, pubkey, { + provider: "cloudflare_r2", + payload: r2Payload, + meta: { provider: "cloudflare_r2" }, + isDefault: true, + }); + const record = await storageService.exportAccountRecord(pubkey); + + let fired = null; + const off = storageService.onConnectionsChanged((e) => { + fired = e; + }); + await storageService.importAccountRecord(pubkey, record); + off(); + + assert.deepEqual( + fired, + { pubkey }, + "a Nostr sync import fires a connection-change event so the open upload modal can refresh", + ); + + // Unsubscribe actually detaches — a second import must not re-fire this listener. + fired = null; + await storageService.importAccountRecord(pubkey, record); + assert.equal(fired, null, "off() detached the observer"); + }); });