feat(blossom): Phase 2 management — blob delete-on-orphan + kind-10063 server list (#30)

- deleteByUrl maps a Blossom URL back to its content-addressed blob (blobSha256FromUrl) and deletes it via BUD-12, defaulting to the URL's own origin. Wired into the edit-modal orphan cleanup so a superseded Blossom video/thumbnail is reclaimed (best-effort, never blocks the edit).

- BUD-03 kind-10063 server-list discovery: read (serversFromServerListEvent) + build (buildBlossomServerListEvent) + an extracted, testable import/publish helper (blossomServerList.js), plus 'Import from my server list' / 'Publish my server list' controls in the Storage pane's Blossom section.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-12 10:32:22 -04:00
1 parent 51072e4871
commit e81e65ff40
9 files changed
+515

No files matched your search

@@ -9,6 +9,10 @@ import {
import { getActiveSigner } from "../../nostr/client.js";
import { FEATURE_BLOSSOM_STORAGE } from "../../constants.js";
import { BLOSSOM_PROVIDER } from "../../services/blossomService.js";
import {
importBlossomServerList,
publishBlossomServerList,
} from "./blossomServerList.js";
import { DEFAULT_NIP07_ENCRYPTION_METHODS } from "../../nostr/nip07Permissions.js";
import { storageSyncService } from "../../services/storageSyncService.js";
import { StorageCorsHelp } from "./storageCorsHelp.js";
@@ -106,6 +110,12 @@ export class ProfileStorageController {
document.getElementById("storageBlossomSection") || null;
this.storageBlossomServersInput =
document.getElementById("storageBlossomServers") || null;
this.storageBlossomImportBtn =
document.getElementById("storageBlossomImportBtn") || null;
this.storageBlossomPublishBtn =
document.getElementById("storageBlossomPublishBtn") || null;
this.storageBlossomServerListStatus =
document.getElementById("storageBlossomServerListStatus") || null;
this.storageProviderBlossomOption =
document.getElementById("storageProviderBlossomOption") || null;
// Reveal the Blossom provider option only when the deployment enables it.
@@ -161,6 +171,18 @@ export class ProfileStorageController {
});
}
if (this.storageBlossomImportBtn instanceof HTMLElement) {
this.storageBlossomImportBtn.addEventListener("click", () => {
void this.handleImportBlossomServerList();
});
}
if (this.storageBlossomPublishBtn instanceof HTMLElement) {
this.storageBlossomPublishBtn.addEventListener("click", () => {
void this.handlePublishBlossomServerList();
});
}
if (this.storageProviderInput instanceof HTMLElement) {
this.storageProviderInput.addEventListener("change", () => {
void this.handleProviderChange();
@@ -560,6 +582,50 @@ export class ProfileStorageController {
this.storageUnlockFailure = null;
}
setBlossomServerListStatus(message, kind = "info") {
if (!(this.storageBlossomServerListStatus instanceof HTMLElement)) return;
this.storageBlossomServerListStatus.textContent = message || "";
const tone =
kind === "error"
? "text-status-danger"
: kind === "success"
? "text-status-success"
: "text-muted";
this.storageBlossomServerListStatus.className = `text-xs ${tone}`;
}
blossomServerListArgs() {
return {
client: this.mainController.services?.nostrClient,
pubkey: this.mainController.normalizeHexPubkey(
this.mainController.getActivePubkey(),
),
};
}
async handleImportBlossomServerList() {
this.setBlossomServerListStatus("Fetching your server list…");
const res = await importBlossomServerList(this.blossomServerListArgs());
if (
res.servers?.length &&
this.storageBlossomServersInput instanceof HTMLElement
) {
this.storageBlossomServersInput.value = res.servers.join("\n");
}
if (res.error) devLogger.warn("[ProfileModal] Blossom import:", res.error);
this.setBlossomServerListStatus(res.message, res.kind);
}
async handlePublishBlossomServerList() {
this.setBlossomServerListStatus("Publishing your server list…");
const res = await publishBlossomServerList({
...this.blossomServerListArgs(),
servers: this.parseBlossomServers(),
});
if (res.error) devLogger.warn("[ProfileModal] Blossom publish:", res.error);
this.setBlossomServerListStatus(res.message, res.kind);
}
// After a page reload, a persisted nsec session restores the logged-in pubkey + UI
// but NOT the in-memory signer (the key is passphrase-encrypted). Storage unlock then
// has no usable signer, and the generic "No active signer" error is misleading.