mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add delete modal for full video removal
This commit is contained in:
1 parent
f4ebb255a2
commit
0a8f3a7b05
5 files changed
+740
-45
No files matched your search
@@ -0,0 +1,71 @@
|
||||
<!-- components/delete-video-modal.html -->
|
||||
<div
|
||||
id="deleteVideoModal"
|
||||
class="bv-modal hidden items-start justify-center md:items-center"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="deleteModalTitle"
|
||||
aria-describedby="deleteModalDescription"
|
||||
data-ds="new"
|
||||
>
|
||||
<div
|
||||
id="deleteVideoModalOverlay"
|
||||
class="bv-modal-backdrop"
|
||||
data-dismiss
|
||||
></div>
|
||||
|
||||
<div class="modal-sheet max-h-modal-shell w-full max-w-lg overflow-y-auto" tabindex="-1">
|
||||
<div class="modal-header">
|
||||
<div class="flex flex-col">
|
||||
<h2 id="deleteModalTitle" class="text-xl font-bold text-text">Delete video</h2>
|
||||
<p id="deleteModalSubtitle" class="text-xs text-muted">
|
||||
This action removes the note from every feed.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
id="closeDeleteVideoModal"
|
||||
class="modal-close"
|
||||
type="button"
|
||||
aria-label="Close delete modal"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body space-y-6">
|
||||
<div class="rounded-md border border-critical/40 bg-critical/10 p-4 text-sm text-critical">
|
||||
<p class="font-semibold">Deleting publishes tombstones for every revision.</p>
|
||||
<p class="mt-2 text-xs">
|
||||
Relays and clients will treat the note as deleted. This cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 text-sm text-text">
|
||||
<p id="deleteModalDescription">
|
||||
Are you sure you want to delete this video? Viewers will no longer see it in any grid.
|
||||
</p>
|
||||
<dl id="deleteModalMetadata" class="grid gap-3 text-xs text-muted sm:grid-cols-2"></dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer flex flex-col gap-3 sm:flex-row sm:justify-end">
|
||||
<button type="button" id="cancelDeleteVideo" class="btn-ghost w-full sm:w-auto">Cancel</button>
|
||||
<button
|
||||
type="button"
|
||||
id="confirmDeleteVideo"
|
||||
class="btn w-full sm:w-auto"
|
||||
data-variant="critical"
|
||||
>
|
||||
Delete all versions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,6 +72,7 @@ import { VideoModal } from "./ui/components/VideoModal.js";
|
||||
import { UploadModal } from "./ui/components/UploadModal.js";
|
||||
import { EditModal } from "./ui/components/EditModal.js";
|
||||
import { RevertModal } from "./ui/components/RevertModal.js";
|
||||
import { DeleteModal } from "./ui/components/DeleteModal.js";
|
||||
import {
|
||||
prepareStaticModal,
|
||||
openStaticModal,
|
||||
@@ -204,6 +205,8 @@ class Application {
|
||||
this.boundEditModalSubmitHandler = null;
|
||||
this.boundEditModalCancelHandler = null;
|
||||
this.boundRevertConfirmHandler = null;
|
||||
this.boundDeleteConfirmHandler = null;
|
||||
this.boundDeleteCancelHandler = null;
|
||||
this.boundVideoModalCloseHandler = null;
|
||||
this.boundVideoModalCopyHandler = null;
|
||||
this.boundVideoModalShareHandler = null;
|
||||
@@ -215,6 +218,7 @@ class Application {
|
||||
this.videoListViewEditHandler = null;
|
||||
this.videoListViewRevertHandler = null;
|
||||
this.videoListViewDeleteHandler = null;
|
||||
this.deleteModal = null;
|
||||
this.moreMenuController = null;
|
||||
this.latestFeedMetadata = null;
|
||||
this.lastModalTrigger = null;
|
||||
@@ -485,6 +489,35 @@ class Application {
|
||||
this.boundRevertConfirmHandler
|
||||
);
|
||||
|
||||
const deleteModalEvents = new EventTarget();
|
||||
this.deleteModal =
|
||||
(typeof ui.deleteModal === "function"
|
||||
? ui.deleteModal({ app: this, eventTarget: deleteModalEvents })
|
||||
: ui.deleteModal) ||
|
||||
new DeleteModal({
|
||||
removeTrackingScripts,
|
||||
setGlobalModalState,
|
||||
truncateMiddle,
|
||||
container: document.getElementById("modalContainer") || null,
|
||||
eventTarget: deleteModalEvents,
|
||||
});
|
||||
|
||||
this.boundDeleteConfirmHandler = (event) => {
|
||||
this.handleDeleteModalConfirm(event);
|
||||
};
|
||||
this.deleteModal.addEventListener(
|
||||
"video:delete-confirm",
|
||||
this.boundDeleteConfirmHandler
|
||||
);
|
||||
|
||||
this.boundDeleteCancelHandler = () => {
|
||||
this.showError("");
|
||||
};
|
||||
this.deleteModal.addEventListener(
|
||||
"video:delete-cancel",
|
||||
this.boundDeleteCancelHandler
|
||||
);
|
||||
|
||||
try {
|
||||
const profileModalContainer = document.getElementById("modalContainer") || null;
|
||||
if (profileModalContainer) {
|
||||
@@ -5967,12 +6000,58 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
async handleDeleteModalConfirm(event) {
|
||||
const detail = event?.detail || {};
|
||||
const targetVideo = detail.video || this.deleteModal?.activeVideo || null;
|
||||
|
||||
if (!targetVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pubkey) {
|
||||
this.showError("Please login to delete videos.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.deleteModal) {
|
||||
this.showError("Delete modal is not available right now.");
|
||||
return;
|
||||
}
|
||||
|
||||
const rootId = targetVideo.videoRootId || targetVideo.id || "";
|
||||
if (!rootId) {
|
||||
this.showError("Unable to determine the video root for deletion.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.deleteModal.setBusy(true, "Deleting…");
|
||||
await this.nostrService.handleFullDeleteVideo({
|
||||
videoRootId: rootId,
|
||||
video: targetVideo,
|
||||
pubkey: this.pubkey,
|
||||
confirm: false,
|
||||
});
|
||||
|
||||
await this.loadVideos();
|
||||
this.showSuccess("All versions deleted successfully!");
|
||||
this.deleteModal.setBusy(false);
|
||||
this.deleteModal.close();
|
||||
this.forceRefreshAllProfiles();
|
||||
} catch (err) {
|
||||
devLogger.error("Failed to delete all versions:", err);
|
||||
this.showError("Failed to delete all versions. Please try again.");
|
||||
this.deleteModal.setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Delete Video" from gear menu.
|
||||
*/
|
||||
async handleFullDeleteVideo(target) {
|
||||
try {
|
||||
const normalizedTarget = this.normalizeActionTarget(target);
|
||||
const { triggerElement } = normalizedTarget;
|
||||
const all = this.nostrService.getFilteredActiveVideos({
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
@@ -5992,28 +6071,19 @@ class Application {
|
||||
this.showError("You do not own this video.");
|
||||
return;
|
||||
}
|
||||
// Make sure the user is absolutely sure:
|
||||
if (
|
||||
!confirm(
|
||||
`Delete ALL versions of "${video.title}"? This action is permanent.`
|
||||
)
|
||||
) {
|
||||
if (!this.deleteModal) {
|
||||
this.showError("Delete modal is not available right now.");
|
||||
return;
|
||||
}
|
||||
|
||||
// We assume video.videoRootId is not empty, or fallback to video.id if needed
|
||||
const rootId = video.videoRootId || video.id;
|
||||
const loaded = await this.deleteModal.load();
|
||||
if (!loaded) {
|
||||
this.showError("Delete modal is not available right now.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.nostrService.handleFullDeleteVideo({
|
||||
videoRootId: rootId,
|
||||
pubkey: this.pubkey,
|
||||
confirm: false,
|
||||
});
|
||||
|
||||
// Reload
|
||||
await this.loadVideos();
|
||||
this.showSuccess("All versions deleted successfully!");
|
||||
this.forceRefreshAllProfiles();
|
||||
this.deleteModal.setVideo(video);
|
||||
this.deleteModal.open({ video }, { triggerElement });
|
||||
} catch (err) {
|
||||
devLogger.error("Failed to delete all versions:", err);
|
||||
this.showError("Failed to delete all versions. Please try again.");
|
||||
@@ -7638,6 +7708,28 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.deleteModal && this.boundDeleteConfirmHandler) {
|
||||
this.deleteModal.removeEventListener(
|
||||
"video:delete-confirm",
|
||||
this.boundDeleteConfirmHandler
|
||||
);
|
||||
this.boundDeleteConfirmHandler = null;
|
||||
}
|
||||
if (this.deleteModal && this.boundDeleteCancelHandler) {
|
||||
this.deleteModal.removeEventListener(
|
||||
"video:delete-cancel",
|
||||
this.boundDeleteCancelHandler
|
||||
);
|
||||
this.boundDeleteCancelHandler = null;
|
||||
}
|
||||
if (typeof this.deleteModal?.destroy === "function") {
|
||||
try {
|
||||
this.deleteModal.destroy();
|
||||
} catch (error) {
|
||||
devLogger.warn("[Application] Failed to destroy delete modal:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.videoModal) {
|
||||
if (this.boundVideoModalCloseHandler) {
|
||||
this.videoModal.removeEventListener(
|
||||
|
||||
+103
-25
@@ -6906,6 +6906,8 @@ export class NostrClient {
|
||||
}
|
||||
|
||||
const shouldConfirm = options?.confirm !== false;
|
||||
const targetVideo =
|
||||
options && typeof options.video === "object" ? options.video : null;
|
||||
let confirmed = true;
|
||||
|
||||
if (shouldConfirm && typeof window?.confirm === "function") {
|
||||
@@ -6919,43 +6921,119 @@ export class NostrClient {
|
||||
return null; // Cancel deletion if user clicks "Cancel"
|
||||
}
|
||||
|
||||
// 1) Find all events in our local allEvents that share the same root.
|
||||
const matchingEvents = [];
|
||||
for (const [id, vid] of this.allEvents.entries()) {
|
||||
if (
|
||||
vid.videoRootId === videoRootId &&
|
||||
vid.pubkey === pubkey &&
|
||||
!vid.deleted
|
||||
) {
|
||||
matchingEvents.push(vid);
|
||||
const normalizedPubkey = typeof pubkey === "string" ? pubkey.toLowerCase() : "";
|
||||
const normalizedRootInput =
|
||||
typeof videoRootId === "string" && videoRootId.trim().length
|
||||
? videoRootId.trim()
|
||||
: "";
|
||||
const inferredRoot =
|
||||
normalizedRootInput ||
|
||||
(targetVideo && typeof targetVideo.videoRootId === "string"
|
||||
? targetVideo.videoRootId.trim()
|
||||
: "");
|
||||
const targetDTag = targetVideo ? getDTagValueFromTags(targetVideo.tags) : "";
|
||||
|
||||
if (targetVideo) {
|
||||
try {
|
||||
await this.hydrateVideoHistory(targetVideo);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[nostr] Failed to hydrate video history before delete:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!matchingEvents.length) {
|
||||
|
||||
const matchingEvents = new Map();
|
||||
for (const candidate of this.allEvents.values()) {
|
||||
if (!candidate || typeof candidate !== "object") {
|
||||
continue;
|
||||
}
|
||||
const candidatePubkey =
|
||||
typeof candidate.pubkey === "string" ? candidate.pubkey.toLowerCase() : "";
|
||||
if (!candidatePubkey || candidatePubkey !== normalizedPubkey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate.deleted === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateRoot =
|
||||
typeof candidate.videoRootId === "string" ? candidate.videoRootId : "";
|
||||
const candidateDTag = getDTagValueFromTags(candidate.tags);
|
||||
const sameRoot = inferredRoot && candidateRoot === inferredRoot;
|
||||
const sameD = targetDTag && candidateDTag === targetDTag;
|
||||
const legacyMatch =
|
||||
!inferredRoot &&
|
||||
targetVideo &&
|
||||
candidateRoot &&
|
||||
candidateRoot === targetVideo.id;
|
||||
const directMatch = targetVideo && candidate.id === targetVideo.id;
|
||||
|
||||
if (!sameRoot && !sameD && !legacyMatch && !directMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matchingEvents.set(candidate.id, candidate);
|
||||
}
|
||||
|
||||
if (targetVideo && !matchingEvents.has(targetVideo.id)) {
|
||||
matchingEvents.set(targetVideo.id, targetVideo);
|
||||
}
|
||||
|
||||
if (!matchingEvents.size) {
|
||||
throw new Error("No existing events found for that root.");
|
||||
}
|
||||
|
||||
// 2) For each event, create a "revert" event to mark it as deleted.
|
||||
// This will prompt the user (via the extension) to sign the deletion.
|
||||
for (const vid of matchingEvents) {
|
||||
for (const vid of matchingEvents.values()) {
|
||||
const baseRoot =
|
||||
(typeof vid.videoRootId === "string" && vid.videoRootId) ||
|
||||
inferredRoot ||
|
||||
(targetVideo && typeof targetVideo.videoRootId === "string"
|
||||
? targetVideo.videoRootId
|
||||
: "") ||
|
||||
(targetVideo ? targetVideo.id : "") ||
|
||||
vid.id;
|
||||
|
||||
const contentPayload = {
|
||||
version: Number.isFinite(vid.version) ? vid.version : 3,
|
||||
deleted: vid.deleted === true,
|
||||
isPrivate: vid.isPrivate === true,
|
||||
isNsfw: vid.isNsfw === true,
|
||||
isForKids: vid.isForKids === true && vid.isNsfw !== true,
|
||||
title: typeof vid.title === "string" ? vid.title : "",
|
||||
url: typeof vid.url === "string" ? vid.url : "",
|
||||
magnet: typeof vid.magnet === "string" ? vid.magnet : "",
|
||||
thumbnail: typeof vid.thumbnail === "string" ? vid.thumbnail : "",
|
||||
description: typeof vid.description === "string" ? vid.description : "",
|
||||
mode: typeof vid.mode === "string" ? vid.mode : "live",
|
||||
videoRootId: baseRoot,
|
||||
};
|
||||
|
||||
await this.revertVideo(
|
||||
{
|
||||
id: vid.id,
|
||||
pubkey: vid.pubkey,
|
||||
content: JSON.stringify({
|
||||
version: vid.version,
|
||||
deleted: vid.deleted,
|
||||
isPrivate: vid.isPrivate,
|
||||
title: vid.title,
|
||||
url: vid.url,
|
||||
magnet: vid.magnet,
|
||||
thumbnail: vid.thumbnail,
|
||||
description: vid.description,
|
||||
mode: vid.mode,
|
||||
}),
|
||||
tags: vid.tags,
|
||||
content: JSON.stringify(contentPayload),
|
||||
tags: Array.isArray(vid.tags) ? vid.tags : [],
|
||||
},
|
||||
pubkey
|
||||
);
|
||||
|
||||
const cached = this.allEvents.get(vid.id) || vid;
|
||||
cached.deleted = true;
|
||||
cached.url = "";
|
||||
cached.magnet = "";
|
||||
cached.thumbnail = "";
|
||||
cached.description = "This version was deleted by the creator.";
|
||||
cached.videoRootId = baseRoot;
|
||||
this.allEvents.set(vid.id, cached);
|
||||
|
||||
const activeKey = getActiveKey(cached);
|
||||
if (activeKey) {
|
||||
this.activeMap.delete(activeKey);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -639,12 +639,13 @@ export class NostrService {
|
||||
return result;
|
||||
}
|
||||
|
||||
async handleFullDeleteVideo({ videoRootId, pubkey, confirm = true } = {}) {
|
||||
async handleFullDeleteVideo({ videoRootId, video, pubkey, confirm = true } = {}) {
|
||||
const result = await this.nostrClient.deleteAllVersions(videoRootId, pubkey, {
|
||||
confirm,
|
||||
video,
|
||||
});
|
||||
if (result) {
|
||||
this.emit("videos:deleted", { videoRootId, pubkey });
|
||||
this.emit("videos:deleted", { videoRootId, video, pubkey });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
import { createModalAccessibility } from "./modalAccessibility.js";
|
||||
|
||||
function extractDTagValue(tags) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return "";
|
||||
}
|
||||
for (const entry of tags) {
|
||||
if (!Array.isArray(entry) || entry.length < 2) {
|
||||
continue;
|
||||
}
|
||||
if (entry[0] === "d" && typeof entry[1] === "string") {
|
||||
return entry[1];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export class DeleteModal {
|
||||
constructor({
|
||||
removeTrackingScripts,
|
||||
setGlobalModalState,
|
||||
truncateMiddle,
|
||||
eventTarget,
|
||||
container,
|
||||
} = {}) {
|
||||
this.removeTrackingScripts =
|
||||
typeof removeTrackingScripts === "function" ? removeTrackingScripts : () => {};
|
||||
this.setGlobalModalState =
|
||||
typeof setGlobalModalState === "function" ? setGlobalModalState : () => {};
|
||||
this.truncateMiddle =
|
||||
typeof truncateMiddle === "function" ? truncateMiddle : (value, maxLength = 48) => {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
if (value.length <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
if (maxLength <= 1) {
|
||||
return value.slice(0, maxLength);
|
||||
}
|
||||
const ellipsis = "…";
|
||||
const charsToShow = maxLength - ellipsis.length;
|
||||
const front = Math.ceil(charsToShow / 2);
|
||||
const back = Math.floor(charsToShow / 2);
|
||||
return `${value.slice(0, front)}${ellipsis}${value.slice(value.length - back)}`;
|
||||
};
|
||||
this.eventTarget = eventTarget instanceof EventTarget ? eventTarget : new EventTarget();
|
||||
this.container = container || document.getElementById("modalContainer") || null;
|
||||
|
||||
this.modal = null;
|
||||
this.overlay = null;
|
||||
this.modalPanel = null;
|
||||
this.title = null;
|
||||
this.subtitle = null;
|
||||
this.description = null;
|
||||
this.metadataList = null;
|
||||
this.confirmButton = null;
|
||||
this.cancelButton = null;
|
||||
this.closeButton = null;
|
||||
|
||||
this.modalAccessibility = null;
|
||||
|
||||
this.activeVideo = null;
|
||||
this.context = null;
|
||||
this.busy = false;
|
||||
this.bound = false;
|
||||
this.confirmDefaultLabel = "Delete all versions";
|
||||
|
||||
this.boundHandlers = {
|
||||
confirm: (event) => this.handleConfirmClick(event),
|
||||
cancel: (event) => this.handleCancelInteraction(event),
|
||||
overlay: (event) => this.handleCancelInteraction(event),
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener(type, listener, options) {
|
||||
this.eventTarget.addEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
removeEventListener(type, listener, options) {
|
||||
this.eventTarget.removeEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
dispatch(type, detail) {
|
||||
this.eventTarget.dispatchEvent(
|
||||
new CustomEvent(type, {
|
||||
detail,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.modal) {
|
||||
return this.modal;
|
||||
}
|
||||
|
||||
const targetContainer = this.container || document.getElementById("modalContainer");
|
||||
if (!targetContainer) {
|
||||
throw new Error("Modal container element not found!");
|
||||
}
|
||||
|
||||
let modal = targetContainer.querySelector("#deleteVideoModal");
|
||||
if (!modal) {
|
||||
const response = await fetch("components/delete-video-modal.html");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = html;
|
||||
this.removeTrackingScripts(wrapper);
|
||||
targetContainer.appendChild(wrapper);
|
||||
modal = wrapper.querySelector("#deleteVideoModal");
|
||||
}
|
||||
|
||||
if (!modal) {
|
||||
throw new Error("Delete video modal markup missing after load.");
|
||||
}
|
||||
|
||||
this.cacheElements(modal);
|
||||
this.setupModalAccessibility();
|
||||
this.bindEvents();
|
||||
this.reset();
|
||||
|
||||
return this.modal;
|
||||
}
|
||||
|
||||
cacheElements(modal) {
|
||||
this.modal = modal;
|
||||
this.overlay = modal.querySelector("#deleteVideoModalOverlay") || null;
|
||||
this.modalPanel = modal.querySelector(".modal-sheet") || modal;
|
||||
this.title = modal.querySelector("#deleteModalTitle") || null;
|
||||
this.subtitle = modal.querySelector("#deleteModalSubtitle") || null;
|
||||
this.description = modal.querySelector("#deleteModalDescription") || null;
|
||||
this.metadataList = modal.querySelector("#deleteModalMetadata") || null;
|
||||
this.confirmButton = modal.querySelector("#confirmDeleteVideo") || null;
|
||||
this.cancelButton = modal.querySelector("#cancelDeleteVideo") || null;
|
||||
this.closeButton = modal.querySelector("#closeDeleteVideoModal") || null;
|
||||
|
||||
if (this.confirmButton) {
|
||||
const text = this.confirmButton.textContent?.trim();
|
||||
if (text) {
|
||||
this.confirmDefaultLabel = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupModalAccessibility() {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalAccessibility = createModalAccessibility({
|
||||
root: this.modal,
|
||||
panel: this.modalPanel || this.modal,
|
||||
backdrop: this.overlay,
|
||||
onRequestClose: () => this.handleCancelInteraction(),
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
if (this.bound || !this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.confirmButton) {
|
||||
this.confirmButton.addEventListener("click", this.boundHandlers.confirm);
|
||||
}
|
||||
if (this.cancelButton) {
|
||||
this.cancelButton.addEventListener("click", this.boundHandlers.cancel);
|
||||
}
|
||||
if (this.closeButton) {
|
||||
this.closeButton.addEventListener("click", this.boundHandlers.cancel);
|
||||
}
|
||||
if (this.overlay) {
|
||||
this.overlay.addEventListener("click", this.boundHandlers.overlay);
|
||||
}
|
||||
|
||||
this.bound = true;
|
||||
}
|
||||
|
||||
open(context = {}, { triggerElement } = {}) {
|
||||
if (!this.modal) {
|
||||
throw new Error("Delete modal has not been loaded.");
|
||||
}
|
||||
|
||||
this.context = context || {};
|
||||
this.modal.classList.remove("hidden");
|
||||
this.modalAccessibility?.activate({ triggerElement });
|
||||
this.setGlobalModalState("deleteVideo", true);
|
||||
this.dispatch("video:delete-open", {
|
||||
context: this.context,
|
||||
video: this.activeVideo,
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = {
|
||||
context: this.context,
|
||||
video: this.activeVideo,
|
||||
};
|
||||
|
||||
this.modal.classList.add("hidden");
|
||||
this.modalAccessibility?.deactivate();
|
||||
this.setGlobalModalState("deleteVideo", false);
|
||||
this.dispatch("video:delete-close", detail);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
setVideo(video) {
|
||||
this.activeVideo = video || null;
|
||||
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const titleText =
|
||||
video && typeof video.title === "string" && video.title.trim().length
|
||||
? `Delete “${video.title.trim()}”`
|
||||
: "Delete video";
|
||||
if (this.title) {
|
||||
this.title.textContent = titleText;
|
||||
}
|
||||
|
||||
const rootId =
|
||||
video && typeof video.videoRootId === "string" && video.videoRootId.trim().length
|
||||
? video.videoRootId.trim()
|
||||
: "";
|
||||
const dTagValue = extractDTagValue(video?.tags);
|
||||
|
||||
if (this.subtitle) {
|
||||
const subtitleParts = [];
|
||||
if (rootId) {
|
||||
subtitleParts.push(`Root ${this.truncateMiddle(rootId, 48)}`);
|
||||
}
|
||||
if (dTagValue) {
|
||||
subtitleParts.push(`d=${this.truncateMiddle(dTagValue, 48)}`);
|
||||
}
|
||||
this.subtitle.textContent = subtitleParts.length
|
||||
? `Marks ${subtitleParts.join(" • ")} as deleted across relays.`
|
||||
: "This action removes the note from every feed.";
|
||||
}
|
||||
|
||||
if (this.description) {
|
||||
const hasTitle =
|
||||
video && typeof video.title === "string" && video.title.trim().length > 0;
|
||||
this.description.textContent = hasTitle
|
||||
? `Deleting will mark every revision of “${video.title.trim()}” as deleted and hide it from viewers.`
|
||||
: "Deleting will mark every revision as deleted and hide it from viewers.";
|
||||
}
|
||||
|
||||
this.renderMetadata(video, { rootId, dTagValue });
|
||||
}
|
||||
|
||||
renderMetadata(video, { rootId, dTagValue } = {}) {
|
||||
if (!this.metadataList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = this.metadataList.ownerDocument;
|
||||
this.metadataList.innerHTML = "";
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = [];
|
||||
const scopeMessage = rootId
|
||||
? "All revisions sharing this root will be tombstoned."
|
||||
: "All revisions linked to this note will be tombstoned.";
|
||||
entries.push({
|
||||
label: "Scope",
|
||||
value: scopeMessage,
|
||||
span: true,
|
||||
});
|
||||
|
||||
if (rootId) {
|
||||
entries.push({
|
||||
label: "Video root",
|
||||
value: this.truncateMiddle(rootId, 60),
|
||||
});
|
||||
}
|
||||
|
||||
if (dTagValue) {
|
||||
entries.push({
|
||||
label: "d tag",
|
||||
value: this.truncateMiddle(dTagValue, 60),
|
||||
});
|
||||
}
|
||||
|
||||
const author =
|
||||
video && typeof video.pubkey === "string" && video.pubkey.trim().length
|
||||
? video.pubkey.trim()
|
||||
: "";
|
||||
if (author) {
|
||||
entries.push({
|
||||
label: "Author",
|
||||
value: this.truncateMiddle(author, 60),
|
||||
});
|
||||
}
|
||||
|
||||
const version = Number.isFinite(video?.version) ? video.version : null;
|
||||
if (version !== null) {
|
||||
entries.push({
|
||||
label: "Active version",
|
||||
value: `${version}`,
|
||||
});
|
||||
}
|
||||
|
||||
const visibility = video?.isPrivate === true ? "Private" : "Public";
|
||||
entries.push({
|
||||
label: "Visibility",
|
||||
value: visibility,
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
const wrapper = doc.createElement("div");
|
||||
wrapper.className = entry.span === true ? "space-y-1 sm:col-span-2" : "space-y-1";
|
||||
|
||||
const dt = doc.createElement("dt");
|
||||
dt.className = "text-2xs font-semibold uppercase tracking-wide text-muted-strong";
|
||||
dt.textContent = entry.label;
|
||||
|
||||
const dd = doc.createElement("dd");
|
||||
dd.className = "text-xs text-muted";
|
||||
dd.textContent = entry.value;
|
||||
|
||||
wrapper.appendChild(dt);
|
||||
wrapper.appendChild(dd);
|
||||
this.metadataList.appendChild(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(isBusy, label = "Deleting…") {
|
||||
this.busy = isBusy === true;
|
||||
if (this.confirmButton) {
|
||||
this.confirmButton.disabled = this.busy;
|
||||
if (this.busy) {
|
||||
this.confirmButton.textContent = label;
|
||||
this.confirmButton.dataset.state = "pending";
|
||||
} else {
|
||||
this.confirmButton.textContent = this.confirmDefaultLabel;
|
||||
this.confirmButton.dataset.state = "";
|
||||
delete this.confirmButton.dataset.state;
|
||||
}
|
||||
}
|
||||
if (this.cancelButton) {
|
||||
this.cancelButton.disabled = this.busy;
|
||||
}
|
||||
if (this.closeButton) {
|
||||
this.closeButton.disabled = this.busy;
|
||||
}
|
||||
if (this.modalPanel) {
|
||||
this.modalPanel.setAttribute("aria-busy", this.busy ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
handleConfirmClick(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatch("video:delete-confirm", {
|
||||
video: this.activeVideo,
|
||||
context: this.context,
|
||||
});
|
||||
}
|
||||
|
||||
handleCancelInteraction(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatch("video:delete-cancel", {
|
||||
video: this.activeVideo,
|
||||
context: this.context,
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.activeVideo = null;
|
||||
this.context = null;
|
||||
this.busy = false;
|
||||
|
||||
if (this.confirmButton) {
|
||||
this.confirmButton.disabled = false;
|
||||
this.confirmButton.textContent = this.confirmDefaultLabel;
|
||||
delete this.confirmButton.dataset.state;
|
||||
}
|
||||
|
||||
if (this.cancelButton) {
|
||||
this.cancelButton.disabled = false;
|
||||
}
|
||||
|
||||
if (this.closeButton) {
|
||||
this.closeButton.disabled = false;
|
||||
}
|
||||
|
||||
if (this.modalPanel) {
|
||||
this.modalPanel.setAttribute("aria-busy", "false");
|
||||
}
|
||||
|
||||
if (this.subtitle) {
|
||||
this.subtitle.textContent = "This action removes the note from every feed.";
|
||||
}
|
||||
|
||||
if (this.description) {
|
||||
this.description.textContent =
|
||||
"Are you sure you want to delete this video? Viewers will no longer see it in any grid.";
|
||||
}
|
||||
|
||||
if (this.metadataList) {
|
||||
this.metadataList.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.confirmButton) {
|
||||
this.confirmButton.removeEventListener("click", this.boundHandlers.confirm);
|
||||
}
|
||||
if (this.cancelButton) {
|
||||
this.cancelButton.removeEventListener("click", this.boundHandlers.cancel);
|
||||
}
|
||||
if (this.closeButton) {
|
||||
this.closeButton.removeEventListener("click", this.boundHandlers.cancel);
|
||||
}
|
||||
if (this.overlay) {
|
||||
this.overlay.removeEventListener("click", this.boundHandlers.overlay);
|
||||
}
|
||||
|
||||
this.modalAccessibility?.destroy();
|
||||
this.modalAccessibility = null;
|
||||
this.bound = false;
|
||||
this.modal = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default DeleteModal;
|
||||
Reference in new issue
Block a user