mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
- 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]>
196 lines
6.5 KiB
JavaScript
196 lines
6.5 KiB
JavaScript
import { devLogger } from "../utils/logger.js";
|
|
import r2Service from "../services/r2Service.js";
|
|
import blossomService, {
|
|
blobSha256FromUrl,
|
|
} from "../services/blossomService.js";
|
|
import { getActiveSigner } from "../nostrClientRegistry.js";
|
|
import { readRelayPublishSummary } from "../nostrPublish.js";
|
|
|
|
export default class EditModalController {
|
|
constructor({ services, state, ui, callbacks, helpers }) {
|
|
this.services = services;
|
|
this.state = state;
|
|
this.ui = ui;
|
|
this.callbacks = callbacks;
|
|
this.helpers = helpers;
|
|
}
|
|
|
|
async open(target) {
|
|
try {
|
|
const normalizedTarget = this.helpers.normalizeActionTarget(target);
|
|
const { triggerElement } = normalizedTarget;
|
|
|
|
const blacklistedEventIds = this.state.getBlacklistedEventIds();
|
|
const isAuthorBlocked = this.callbacks.isAuthorBlocked;
|
|
|
|
const latestVideos = await this.services.nostrService.fetchVideos({
|
|
blacklistedEventIds,
|
|
isAuthorBlocked: (pubkey) => isAuthorBlocked(pubkey),
|
|
});
|
|
|
|
const video = await this.helpers.resolveVideoActionTarget({
|
|
...normalizedTarget,
|
|
preloadedList: latestVideos,
|
|
});
|
|
|
|
// 2) Basic ownership checks
|
|
const pubkey = this.state.getPubkey();
|
|
if (!pubkey) {
|
|
this.ui.showError("Please login to edit videos.");
|
|
return;
|
|
}
|
|
const userPubkey = (pubkey || "").toLowerCase();
|
|
const videoPubkey = (video?.pubkey || "").toLowerCase();
|
|
if (!video || !videoPubkey || videoPubkey !== userPubkey) {
|
|
this.ui.showError("You do not own this video.");
|
|
return;
|
|
}
|
|
|
|
const editModal = this.ui.getEditModal();
|
|
if (!editModal) {
|
|
this.ui.showError("Edit modal is not available right now.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await editModal.load();
|
|
} catch (error) {
|
|
devLogger.error("Failed to load edit modal:", error);
|
|
this.ui.showError(`Failed to initialize edit modal: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await editModal.open(video, { triggerElement });
|
|
} catch (error) {
|
|
devLogger.error("Failed to open edit modal:", error);
|
|
this.ui.showError("Edit modal is not available right now.");
|
|
}
|
|
} catch (err) {
|
|
devLogger.error("Failed to edit video:", err);
|
|
this.ui.showError("Failed to edit video. Please try again.");
|
|
}
|
|
}
|
|
|
|
async handleSubmit(event) {
|
|
const detail = event?.detail || {};
|
|
const { originalEvent, updatedData } = detail;
|
|
if (!originalEvent || !updatedData) {
|
|
return;
|
|
}
|
|
|
|
const pubkey = this.state.getPubkey();
|
|
const editModal = this.ui.getEditModal();
|
|
|
|
if (!pubkey) {
|
|
this.ui.showError("Please login to edit videos.");
|
|
if (editModal?.setSubmitState) {
|
|
editModal.setSubmitState({ pending: false });
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const editedEvent = await this.services.nostrService.handleEditVideoSubmit({
|
|
originalEvent,
|
|
updatedData,
|
|
pubkey,
|
|
});
|
|
|
|
// Best-effort: when the hosted video URL and/or thumbnail were genuinely
|
|
// replaced, the old R2/S3 objects are now orphaned — remove them. For each,
|
|
// we require both old and new values to be non-empty and different so we
|
|
// never delete an object the new note still references (e.g. the URL was
|
|
// merely cleared, or the thumbnail was left unchanged). The video entry
|
|
// also takes its sibling .torrent. Never blocks the edit.
|
|
try {
|
|
const orphans = [];
|
|
|
|
const oldUrl = detail.video?.url || "";
|
|
const newUrl = updatedData?.url || "";
|
|
if (updatedData?.urlEdited && oldUrl && newUrl && oldUrl !== newUrl) {
|
|
// collectVideoStorageKeys derives the video key + its .torrent.
|
|
orphans.push({ url: oldUrl });
|
|
}
|
|
|
|
const oldThumb = detail.video?.thumbnail || "";
|
|
const newThumb = updatedData?.thumbnail || "";
|
|
if (oldThumb && newThumb && oldThumb !== newThumb) {
|
|
orphans.push({ thumbnail: oldThumb });
|
|
}
|
|
|
|
if (orphans.length) {
|
|
const cleanup = await r2Service.deleteVideoStorage({
|
|
videos: orphans,
|
|
pubkey,
|
|
});
|
|
if (cleanup?.deleted?.length) {
|
|
devLogger.log(
|
|
`[edit] Removed ${cleanup.deleted.length} superseded storage object(s).`
|
|
);
|
|
}
|
|
|
|
// Blossom orphans are content-addressed blobs, not R2/S3 keys — delete
|
|
// them via BUD-12 (best-effort, from each blob URL's own origin).
|
|
const blossomUrls = orphans
|
|
.map((o) => o.url || o.thumbnail || "")
|
|
.filter((u) => blobSha256FromUrl(u));
|
|
if (blossomUrls.length) {
|
|
const activeSigner = getActiveSigner();
|
|
if (activeSigner && typeof activeSigner.signEvent === "function") {
|
|
const signer = (draft) => activeSigner.signEvent(draft);
|
|
for (const url of blossomUrls) {
|
|
const res = await blossomService.deleteByUrl({ url, signer });
|
|
if (res.deleted?.length) {
|
|
devLogger.log(
|
|
`[edit] Deleted superseded Blossom blob ${res.sha256}.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (cleanupErr) {
|
|
devLogger.warn(
|
|
"[edit] Storage cleanup failed (edit still succeeded):",
|
|
cleanupErr
|
|
);
|
|
}
|
|
|
|
await this.callbacks.loadVideos();
|
|
|
|
const videosMap = this.state.getVideosMap();
|
|
if (videosMap) {
|
|
videosMap.clear();
|
|
}
|
|
|
|
// #49: an all-timeout publish reaches here flagged unconfirmed — say so
|
|
// honestly instead of claiming a confirmed update.
|
|
const tally = readRelayPublishSummary(editedEvent);
|
|
if (tally?.unconfirmed === true && !(tally.accepted > 0)) {
|
|
this.ui.showSuccess(
|
|
"Video update sent, but no relay has confirmed it yet — it may take a moment to propagate.",
|
|
);
|
|
} else {
|
|
this.ui.showSuccess("Video updated successfully!");
|
|
}
|
|
|
|
if (editModal?.setSubmitState) {
|
|
editModal.setSubmitState({ pending: false });
|
|
}
|
|
|
|
if (typeof editModal.close === "function") {
|
|
editModal.close();
|
|
}
|
|
|
|
this.callbacks.forceRefreshAllProfiles();
|
|
} catch (error) {
|
|
devLogger.error("Failed to edit video:", error);
|
|
this.ui.showError("Failed to edit video. Please try again.");
|
|
if (editModal?.setSubmitState) {
|
|
editModal.setSubmitState({ pending: false });
|
|
}
|
|
}
|
|
}
|
|
}
|