mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
1 parent
d23cfa6697
commit
4906745bff
4 files changed
+88
-1
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user