feat(storage): live-refresh the open upload modal when connections sync from Nostr

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-11 19:04:06 -04:00
1 parent d23cfa6697
commit 4906745bff
4 files changed
+88 -1

No files matched your search

+27
View File
@@ -194,6 +194,31 @@ export class StorageService {
this.dbPromise = null;
// Cache decrypted master keys in memory: Map<pubkey, CryptoKey>
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;
}
+32
View File
@@ -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;
+1 -1
View File
@@ -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,
+28
View File
@@ -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");
});
});