feat(my-videos): thumbnail + Edit/Delete row actions (Phase 1, step 5)

Each My Videos row now shows the video's thumbnail (lazy, with a placeholder box
that survives a broken/missing image) and, for active videos, Edit and Delete
buttons that route to the app's existing handleEditVideo / handleFullDeleteVideo
flows (closing the full-screen profile modal first so those modals aren't hidden
behind it). Delete reuses the app flow that already cleans the live URL's storage.

Deleted videos intentionally have no per-row action yet: the tombstone scrubbed
their URL, so reclaiming their orphaned bucket file requires the Phase 2 bucket
listing/reconciliation (and Restore) — added there so it's done correctly.

Build + full lint green (DOM via createElement/textContent, no innerHTML).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-20 14:43:14 -04:00
1 parent ff04fcbd4b
commit f847bce0d5
1 file changed
+71 -2
+71 -2
View File
@@ -1,5 +1,6 @@
import { devLogger } from "../../utils/logger.js";
import { nostrClient } from "../../nostrClientFacade.js";
import { getApplication } from "../../applicationContext.js";
import { collapseUserVideos } from "./myVideosData.js";
import { classifyVideoHealth, isUrlUnderBase } from "./myVideosHealth.js";
@@ -179,10 +180,11 @@ export class MyVideosController {
});
const li = document.createElement("li");
li.className =
"card flex items-center justify-between gap-3 p-3 border border-border/60";
li.className = "card flex items-center gap-3 p-3 border border-border/60";
li.dataset.health = health.status;
li.appendChild(this.buildThumb(video));
const info = document.createElement("div");
info.className = "min-w-0 flex-1";
@@ -208,9 +210,76 @@ export class MyVideosController {
info.appendChild(titleRow);
info.appendChild(meta);
li.appendChild(info);
li.appendChild(this.buildActions(video));
return li;
}
buildThumb(video) {
const wrap = document.createElement("div");
wrap.className =
"h-12 w-20 shrink-0 overflow-hidden rounded bg-surface-strong/40";
const url = typeof video.thumbnail === "string" ? video.thumbnail.trim() : "";
if (url) {
const img = document.createElement("img");
img.className = "h-full w-full object-cover";
img.loading = "lazy";
img.alt = "";
img.src = url;
// A broken/missing thumbnail leaves the placeholder box rather than the
// browser's broken-image glyph.
img.addEventListener("error", () => img.remove());
wrap.appendChild(img);
}
return wrap;
}
buildActions(video) {
const actions = document.createElement("div");
actions.className = "flex items-center gap-2 shrink-0";
// Active videos can be edited or deleted. Cleaning up a DELETED video's
// orphaned file needs the bucket listing (the tombstone scrubbed its URL),
// so that action arrives in Phase 2 (storage reconciliation).
if (!video.deleted) {
actions.appendChild(
this.buildActionButton("Edit", () => this.handleEdit(video)),
);
actions.appendChild(
this.buildActionButton("Delete", () => this.handleDelete(video), true),
);
}
return actions;
}
buildActionButton(label, onClick, danger = false) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = danger
? "btn-ghost focus-ring text-xs text-status-danger"
: "btn-ghost focus-ring text-xs";
btn.textContent = label;
btn.addEventListener("click", onClick);
return btn;
}
handleEdit(video) {
const app = getApplication();
// The edit modal is a separate top-level surface; close the (full-screen)
// profile modal first so it isn't hidden behind it.
this.mainController.hide();
if (app && typeof app.handleEditVideo === "function") {
void app.handleEditVideo({ video });
}
}
handleDelete(video) {
const app = getApplication();
this.mainController.hide();
if (app && typeof app.handleFullDeleteVideo === "function") {
void app.handleFullDeleteVideo({ video });
}
}
describeSource(video) {
const parts = [];
if (typeof video.url === "string" && video.url.trim()) {