From 9d3a0df02a2b3b62e64fcacba176e0e62bdbabd0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:00:43 -0400 Subject: [PATCH] feat(my-videos): wire bucket reconciliation into the tab (Phase 2, step 3b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the note-side list renders, the My Videos tab now reconciles against the bucket: lists the user's objects (unlock-gated), then marks any active note whose hosted file is gone as a 🟠 "Missing file", and lists bucket objects no current video references in an "Orphaned storage objects" section — including files left behind by videos deleted on Nostr (the headline cleanup case). Each orphan has a "Delete file" button (with a confirm) that calls deleteStorageKeys and refreshes. Reconciliation runs after the list is shown so the UI stays responsive, and is a no-op (orphan section hidden) when storage is locked/unconfigured. Rows carry a data-video-id + a data-role pill so missing status can be applied in place. Build + full lint green (DOM via createElement/textContent, no innerHTML). Co-Authored-By: Claude Opus 4.8 --- components/profile-modal.html | 14 +++ js/ui/profileModal/MyVideosController.js | 125 +++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/components/profile-modal.html b/components/profile-modal.html index 559097ba..6cebbf32 100644 --- a/components/profile-modal.html +++ b/components/profile-modal.html @@ -993,6 +993,20 @@ class="grid-stack" data-orientation="vertical" > +
{ + void this.handleDeleteOrphan(key); + }, true); + + li.appendChild(label); + li.appendChild(del); + return li; + } + + async handleDeleteOrphan(key) { + const r2Service = this.mainController.services?.r2Service; + if (!r2Service || typeof r2Service.deleteStorageKeys !== "function") { + return; + } + if (typeof window !== "undefined" && typeof window.confirm === "function") { + if (!window.confirm(`Permanently delete this file from your bucket?\n\n${key}`)) { + return; + } + } + let result; + try { + result = await r2Service.deleteStorageKeys({ keys: [key], pubkey: this.pubkey }); + } catch (err) { + devLogger.warn("[myVideos] deleteStorageKeys failed:", err); + this.mainController.showError?.("Failed to delete the file. Please try again."); + return; + } + if (result?.ok && result.deleted?.length) { + this.mainController.showSuccess?.("Removed the orphaned file from your bucket."); + void this.populate({ forceFetch: false }); + } else if (result?.reason === "storage-locked") { + this.mainController.showError?.("Unlock storage first, then delete the file."); + } else { + this.mainController.showError?.("Failed to delete the file. Please try again."); + } + } + buildSummary(rows) { if (!rows.length) { return ""; @@ -182,6 +303,9 @@ export class MyVideosController { const li = document.createElement("li"); li.className = "card flex items-center gap-3 p-3 border border-border/60"; li.dataset.health = health.status; + if (typeof video.id === "string" && video.id) { + li.dataset.videoId = video.id; + } li.appendChild(this.buildThumb(video)); @@ -199,6 +323,7 @@ export class MyVideosController { const pill = document.createElement("span"); pill.className = `${PILL_BASE} ${PILL_BY_SEVERITY[health.severity] || PILL_BY_SEVERITY.info}`; pill.textContent = health.label; + pill.dataset.role = "health-pill"; titleRow.appendChild(title); titleRow.appendChild(pill);