added revert video function and delete all previous version functions and added better fetching for profile images and names

This commit is contained in:
Keep Creating Online
2025-02-01 18:41:55 -05:00
parent f5a1b6471b
commit c0b2388b0c
2 changed files with 377 additions and 191 deletions

View File

@@ -84,6 +84,19 @@ class bitvidApp {
document.getElementById("closeLoginModal") || null; document.getElementById("closeLoginModal") || null;
} }
forceRefreshAllProfiles() {
// 1) Grab the newest set of videos from nostrClient
const activeVideos = nostrClient.getActiveVideos();
// 2) Build a unique set of pubkeys
const uniqueAuthors = new Set(activeVideos.map((v) => v.pubkey));
// 3) For each author, fetchAndRenderProfile with forceRefresh = true
for (const authorPubkey of uniqueAuthors) {
this.fetchAndRenderProfile(authorPubkey, true);
}
}
async init() { async init() {
try { try {
// 1. Initialize the video modal (components/video-modal.html) // 1. Initialize the video modal (components/video-modal.html)
@@ -521,7 +534,7 @@ class bitvidApp {
this.uploadModal.classList.add("hidden"); this.uploadModal.classList.add("hidden");
} }
// Refresh the video list // *** Refresh to show the newly uploaded video in the grid ***
await this.loadVideos(); await this.loadVideos();
this.showSuccess("Video shared successfully!"); this.showSuccess("Video shared successfully!");
} catch (err) { } catch (err) {
@@ -533,17 +546,16 @@ class bitvidApp {
/** /**
* Called upon successful login. * Called upon successful login.
*/ */
login(pubkey, saveToStorage = true) { async login(pubkey, saveToStorage = true) {
console.log("[app.js] login() called with pubkey =", pubkey); console.log("[app.js] login() called with pubkey =", pubkey);
this.pubkey = pubkey; this.pubkey = pubkey;
// Hide the login button if present // Hide login button if present
if (this.loginButton) { if (this.loginButton) {
this.loginButton.classList.add("hidden"); this.loginButton.classList.add("hidden");
} }
// Optionally hide logout or userStatus
// Hide logout / userStatus if youre not using them
if (this.logoutButton) { if (this.logoutButton) {
this.logoutButton.classList.add("hidden"); this.logoutButton.classList.add("hidden");
} }
@@ -551,7 +563,7 @@ class bitvidApp {
this.userStatus.classList.add("hidden"); this.userStatus.classList.add("hidden");
} }
// IMPORTANT: Unhide the Upload & Profile buttons // Show the upload button, profile button, etc.
if (this.uploadButton) { if (this.uploadButton) {
this.uploadButton.classList.remove("hidden"); this.uploadButton.classList.remove("hidden");
} }
@@ -559,25 +571,33 @@ class bitvidApp {
this.profileButton.classList.remove("hidden"); this.profileButton.classList.remove("hidden");
} }
// Optionally load the user's own profile // (Optional) load the user's own Nostr profile
this.loadOwnProfile(pubkey); this.loadOwnProfile(pubkey);
// Save pubkey in localStorage (if desired) // Save pubkey locally if requested
if (saveToStorage) { if (saveToStorage) {
localStorage.setItem("userPubKey", pubkey); localStorage.setItem("userPubKey", pubkey);
} }
// Refresh the video list so the user sees any private videos, etc.
await this.loadVideos();
// Force a fresh fetch of all profile pictures/names
this.forceRefreshAllProfiles();
} }
/** /**
* Logout logic * Logout logic
*/ */
logout() { async logout() {
nostrClient.logout(); nostrClient.logout();
this.pubkey = null; this.pubkey = null;
// Show login again (if it exists)
// Show the login button again
if (this.loginButton) { if (this.loginButton) {
this.loginButton.classList.remove("hidden"); this.loginButton.classList.remove("hidden");
} }
// Hide logout or userStatus // Hide logout or userStatus
if (this.logoutButton) { if (this.logoutButton) {
this.logoutButton.classList.add("hidden"); this.logoutButton.classList.add("hidden");
@@ -588,6 +608,7 @@ class bitvidApp {
if (this.userPubKey) { if (this.userPubKey) {
this.userPubKey.textContent = ""; this.userPubKey.textContent = "";
} }
// Hide upload & profile // Hide upload & profile
if (this.uploadButton) { if (this.uploadButton) {
this.uploadButton.classList.add("hidden"); this.uploadButton.classList.add("hidden");
@@ -595,8 +616,15 @@ class bitvidApp {
if (this.profileButton) { if (this.profileButton) {
this.profileButton.classList.add("hidden"); this.profileButton.classList.add("hidden");
} }
// Clear localStorage // Clear localStorage
localStorage.removeItem("userPubKey"); localStorage.removeItem("userPubKey");
// Refresh the video list so user sees only public videos again
await this.loadVideos();
// Force a fresh fetch of all profile pictures/names (public ones in this case)
this.forceRefreshAllProfiles();
} }
/** /**
@@ -653,15 +681,15 @@ class bitvidApp {
// js/app.js // js/app.js
async loadVideos() { async loadVideos() {
console.log("Starting loadVideos (subscription approach)..."); console.log("Starting loadVideos...");
// If you had an existing subscription, unsubscribe first: // 1) If there's an existing subscription, unsubscribe it
if (this.videoSubscription) { if (this.videoSubscription) {
this.videoSubscription.unsub(); this.videoSubscription.unsub();
this.videoSubscription = null; this.videoSubscription = null;
} }
// Optionally show "loading videos..." message // 2) Show "Loading..." message
if (this.videoList) { if (this.videoList) {
this.videoList.innerHTML = ` this.videoList.innerHTML = `
<p class="text-center text-gray-500"> <p class="text-center text-gray-500">
@@ -669,41 +697,24 @@ class bitvidApp {
</p>`; </p>`;
} }
// Clear your local map
this.videosMap.clear();
try { try {
// Subscribe to new events from nostrClient // 3) Force a bulk fetch
await nostrClient.fetchVideos();
// 4) Instead of reusing the entire fetched array,
// use getActiveVideos() for the final display:
const newestActive = nostrClient.getActiveVideos();
this.renderVideoList(newestActive);
// 5) Subscribe for updates
this.videoSubscription = nostrClient.subscribeVideos((video) => { this.videoSubscription = nostrClient.subscribeVideos((video) => {
// If the video is marked deleted, remove it from your local collection // Whenever we get a new or updated event, re-render the newest set:
if (video.deleted) { const activeAll = nostrClient.getActiveVideos();
if (this.videosMap.has(video.id)) { this.renderVideoList(activeAll);
this.videosMap.delete(video.id);
// Now rebuild the list
const allVideos = Array.from(this.videosMap.values());
const newestPerRoot = dedupeToNewestByRoot(allVideos);
this.renderVideoList(newestPerRoot);
}
return;
}
// Skip private videos if they do not belong to the current user
if (video.isPrivate && video.pubkey !== this.pubkey) {
return;
}
// Only add if it's not in the map
if (!this.videosMap.has(video.id)) {
this.videosMap.set(video.id, video);
// Re-run the dedupe logic
const allVideos = Array.from(this.videosMap.values());
const newestPerRoot = dedupeToNewestByRoot(allVideos);
this.renderVideoList(newestPerRoot);
}
}); });
} catch (err) { } catch (err) {
console.error("Subscription error:", err); console.error("Could not load videos:", err);
this.showError("Could not load videos via subscription."); this.showError("Could not load videos from relays.");
if (this.videoList) { if (this.videoList) {
this.videoList.innerHTML = ` this.videoList.innerHTML = `
<p class="text-center text-gray-500"> <p class="text-center text-gray-500">
@@ -714,22 +725,40 @@ class bitvidApp {
} }
/** /**
* Build the DOM for the video list. * Returns true if there's at least one strictly older version
* (same videoRootId, created_at < current) which is NOT deleted.
*/ */
hasOlderVersion(video, allEvents) {
if (!video || !video.videoRootId) return false;
const rootId = video.videoRootId;
const currentTs = video.created_at;
// among ALL known events (including overshadowed), find older, not deleted
const olderMatches = allEvents.filter(
(v) => v.videoRootId === rootId && v.created_at < currentTs && !v.deleted
);
return olderMatches.length > 0;
}
// 4) Build the DOM for each video in newestActive
async renderVideoList(videos) { async renderVideoList(videos) {
if (!this.videoList) return; if (!this.videoList) return;
if (!videos || videos.length === 0) { if (!videos || videos.length === 0) {
this.videoList.innerHTML = ` this.videoList.innerHTML = `
<p class="text-center text-gray-500"> <p class="text-center text-gray-500">
No public videos available yet. Be the first to upload one! No public videos available yet. Be the first to upload one!
</p>`; </p>`;
return; return;
} }
// Sort newest first // Sort newest first
videos.sort((a, b) => b.created_at - a.created_at); videos.sort((a, b) => b.created_at - a.created_at);
// <-- NEW: Convert allEvents map => array to check older overshadowed events
const fullAllEventsArray = Array.from(nostrClient.allEvents.values());
const htmlList = videos.map((video, index) => { const htmlList = videos.map((video, index) => {
if (!video.id || !video.title) { if (!video.id || !video.title) {
console.error("Video missing ID/title:", video); console.error("Video missing ID/title:", video);
@@ -747,45 +776,64 @@ class bitvidApp {
: "border-none"; : "border-none";
const timeAgo = this.formatTimeAgo(video.created_at); const timeAgo = this.formatTimeAgo(video.created_at);
// Gear menu if canEdit // 1) Do we have an older version?
const gearMenu = canEdit let hasOlder = false;
if (canEdit && video.videoRootId) {
hasOlder = this.hasOlderVersion(video, fullAllEventsArray);
}
// 2) If we do => show revert button
const revertButton = hasOlder
? ` ? `
<div class="relative inline-block ml-3 overflow-visible"> <button
<button class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
type="button" onclick="app.handleRevertVideo(${index}); document.getElementById('settingsDropdown-${index}').classList.add('hidden');"
class="inline-flex items-center p-2 rounded-full text-gray-400 hover:text-gray-200 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500" >
onclick="document.getElementById('settingsDropdown-${index}').classList.toggle('hidden')" Revert
> </button>
<img
src="assets/svg/video-settings-gear.svg"
alt="Settings"
class="w-5 h-5"
/>
</button>
<div
id="settingsDropdown-${index}"
class="hidden absolute right-0 bottom-full mb-2 w-32 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
>
<div class="py-1">
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
onclick="app.handleEditVideo(${index}); document.getElementById('settingsDropdown-${index}').classList.add('hidden');"
>
Edit
</button>
<button
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
onclick="app.handleDeleteVideo(${index}); document.getElementById('settingsDropdown-${index}').classList.add('hidden');"
>
Delete
</button>
</div>
</div>
</div>
` `
: ""; : "";
// Build card // 3) Gear menu
const gearMenu = canEdit
? `
<div class="relative inline-block ml-3 overflow-visible">
<button
type="button"
class="inline-flex items-center p-2 rounded-full text-gray-400 hover:text-gray-200 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
onclick="document.getElementById('settingsDropdown-${index}').classList.toggle('hidden')"
>
<img
src="assets/svg/video-settings-gear.svg"
alt="Settings"
class="w-5 h-5"
/>
</button>
<div
id="settingsDropdown-${index}"
class="hidden absolute right-0 bottom-full mb-2 w-32 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
>
<div class="py-1">
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
onclick="app.handleEditVideo(${index}); document.getElementById('settingsDropdown-${index}').classList.add('hidden');"
>
Edit
</button>
${revertButton}
<button
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
onclick="app.handleFullDeleteVideo(${index}); document.getElementById('settingsDropdown-${index}').classList.add('hidden');"
>
Delete All
</button>
</div>
</div>
</div>
`
: "";
// 4) Build the card markup...
const cardHtml = ` const cardHtml = `
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}"> <div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
<a <a
@@ -850,34 +898,44 @@ class bitvidApp {
</div> </div>
</div> </div>
</div> </div>
`; `;
// Kick off a background fetch for the profile // Fire off a background fetch for the author's profile
this.fetchAndRenderProfile(video.pubkey); this.fetchAndRenderProfile(video.pubkey);
return cardHtml; return cardHtml;
}); });
// Filter out any empty strings
const valid = htmlList.filter((x) => x.length > 0); const valid = htmlList.filter((x) => x.length > 0);
if (valid.length === 0) { if (valid.length === 0) {
this.videoList.innerHTML = ` this.videoList.innerHTML = `
<p class="text-center text-gray-500"> <p class="text-center text-gray-500">
No valid videos to display. No valid videos to display.
</p>`; </p>`;
return; return;
} }
// Finally inject into DOM
this.videoList.innerHTML = valid.join(""); this.videoList.innerHTML = valid.join("");
} }
/** /**
* Retrieve the profile for a given pubkey (kind:0) and update the DOM. * Retrieve the profile for a given pubkey (kind:0) and update the DOM.
*/ */
async fetchAndRenderProfile(pubkey) { async fetchAndRenderProfile(pubkey, forceRefresh = false) {
if (this.profileCache.has(pubkey)) { const now = Date.now();
this.updateProfileInDOM(pubkey, this.profileCache.get(pubkey));
// Check if we already have a cached entry for this pubkey:
const cacheEntry = this.profileCache.get(pubkey);
// If not forcing refresh, and we have a cache entry less than 60 sec old, use it:
if (!forceRefresh && cacheEntry && now - cacheEntry.timestamp < 60000) {
this.updateProfileInDOM(pubkey, cacheEntry.profile);
return; return;
} }
// Otherwise, go fetch from the relay
try { try {
const userEvents = await nostrClient.pool.list(nostrClient.relays, [ const userEvents = await nostrClient.pool.list(nostrClient.relays, [
{ kinds: [0], authors: [pubkey], limit: 1 }, { kinds: [0], authors: [pubkey], limit: 1 },
@@ -888,11 +946,18 @@ class bitvidApp {
name: data.name || data.display_name || "Unknown", name: data.name || data.display_name || "Unknown",
picture: data.picture || "assets/jpg/default-profile.jpg", picture: data.picture || "assets/jpg/default-profile.jpg",
}; };
this.profileCache.set(pubkey, profile);
// Store into the cache with a timestamp
this.profileCache.set(pubkey, {
profile,
timestamp: now,
});
// Now update the DOM elements
this.updateProfileInDOM(pubkey, profile); this.updateProfileInDOM(pubkey, profile);
} }
} catch (err) { } catch (err) {
console.error("Profile fetch error:", err); console.error("Profile fetch error for pubkey:", pubkey, err);
} }
} }
@@ -1027,8 +1092,11 @@ class bitvidApp {
*/ */
async handleEditVideo(index) { async handleEditVideo(index) {
try { try {
// 1) Fetch the current list of videos (the newest versions)
const all = await nostrClient.fetchVideos(); const all = await nostrClient.fetchVideos();
const video = all[index]; const video = all[index];
// 2) Basic ownership checks
if (!this.pubkey) { if (!this.pubkey) {
this.showError("Please login to edit videos."); this.showError("Please login to edit videos.");
return; return;
@@ -1038,7 +1106,7 @@ class bitvidApp {
return; return;
} }
// Prompt for updated fields // 3) Prompt the user for updated fields
const newTitle = prompt("New Title? (blank=keep existing)", video.title); const newTitle = prompt("New Title? (blank=keep existing)", video.title);
const newMagnet = prompt( const newMagnet = prompt(
"New Magnet? (blank=keep existing)", "New Magnet? (blank=keep existing)",
@@ -1054,7 +1122,7 @@ class bitvidApp {
); );
const wantPrivate = confirm("Make this video private? OK=Yes, Cancel=No"); const wantPrivate = confirm("Make this video private? OK=Yes, Cancel=No");
// Build updated data, falling back to old values // 4) Build final updated fields (or fallback to existing)
const title = const title =
!newTitle || !newTitle.trim() ? video.title : newTitle.trim(); !newTitle || !newTitle.trim() ? video.title : newTitle.trim();
const magnet = const magnet =
@@ -1064,6 +1132,7 @@ class bitvidApp {
const description = const description =
!newDesc || !newDesc.trim() ? video.description : newDesc.trim(); !newDesc || !newDesc.trim() ? video.description : newDesc.trim();
// 5) Create an object with the new data
const updatedData = { const updatedData = {
version: video.version || 2, version: video.version || 2,
isPrivate: wantPrivate, isPrivate: wantPrivate,
@@ -1074,27 +1143,81 @@ class bitvidApp {
mode: isDevMode ? "dev" : "live", mode: isDevMode ? "dev" : "live",
}; };
// IMPORTANT: we only pass id and pubkey to avoid reusing the old d-tag // 6) Build the originalEvent stub, now including videoRootId to avoid extra fetch
// (Do NOT pass video.tags!) const originalEvent = {
const originalEvent = video; id: video.id,
pubkey: video.pubkey,
videoRootId: video.videoRootId, // <-- pass this if it exists
};
// 7) Call the editVideo method
await nostrClient.editVideo(originalEvent, updatedData, this.pubkey); await nostrClient.editVideo(originalEvent, updatedData, this.pubkey);
this.showSuccess("Video updated successfully!"); // 8) Refresh local UI
await this.loadVideos(); await this.loadVideos();
this.showSuccess("Video updated successfully!");
// 9) Also refresh all profile caches so any new name/pic changes are reflected
this.forceRefreshAllProfiles();
} catch (err) { } catch (err) {
this.log("Failed to edit video:", err.message); console.error("Failed to edit video:", err);
this.showError("Failed to edit video. Please try again."); this.showError("Failed to edit video. Please try again.");
} }
} }
async handleRevertVideo(index) {
try {
// 1) Still use fetchVideos to get the video in question
const activeVideos = await nostrClient.fetchVideos();
const video = activeVideos[index];
if (!this.pubkey) {
this.showError("Please login to revert.");
return;
}
if (!video || video.pubkey !== this.pubkey) {
this.showError("You do not own this video.");
return;
}
// 2) Grab all known events so older overshadowed ones are included
const allEvents = Array.from(nostrClient.allEvents.values());
// 3) Check for older versions among *all* events, not just the active ones
if (!this.hasOlderVersion(video, allEvents)) {
this.showError("No older version exists to revert to.");
return;
}
if (!confirm(`Revert current version of "${video.title}"?`)) {
return;
}
const originalEvent = {
id: video.id,
pubkey: video.pubkey,
tags: video.tags,
};
await nostrClient.revertVideo(originalEvent, this.pubkey);
await this.loadVideos();
this.showSuccess("Current version reverted successfully!");
this.forceRefreshAllProfiles();
} catch (err) {
console.error("Failed to revert video:", err);
this.showError("Failed to revert video. Please try again.");
}
}
/** /**
* Handle "Delete Video" from gear menu. * Handle "Delete Video" from gear menu.
*/ */
async handleDeleteVideo(index) { async handleFullDeleteVideo(index) {
try { try {
const all = await nostrClient.fetchVideos(); const all = await nostrClient.fetchVideos();
const video = all[index]; const video = all[index];
if (!this.pubkey) { if (!this.pubkey) {
this.showError("Please login to delete videos."); this.showError("Please login to delete videos.");
return; return;
@@ -1103,23 +1226,27 @@ class bitvidApp {
this.showError("You do not own this video."); this.showError("You do not own this video.");
return; return;
} }
if (!confirm(`Delete "${video.title}"? This can't be undone.`)) { // Make sure the user is absolutely sure:
if (
!confirm(
`Delete ALL versions of "${video.title}"? This action is permanent.`
)
) {
return; return;
} }
// Only id and pubkey (omit old tags), so that delete doesn't overshadow the old d-tag // We assume video.videoRootId is not empty, or fallback to video.id if needed
const originalEvent = { const rootId = video.videoRootId || video.id;
id: video.id,
pubkey: video.pubkey,
};
await nostrClient.deleteVideo(originalEvent, this.pubkey); await nostrClient.deleteAllVersions(rootId, this.pubkey);
this.showSuccess("Video deleted successfully!"); // Reload
await this.loadVideos(); await this.loadVideos();
this.showSuccess("All versions deleted successfully!");
this.forceRefreshAllProfiles();
} catch (err) { } catch (err) {
this.log("Failed to delete video:", err.message); console.error("Failed to delete all versions:", err);
this.showError("Failed to delete video. Please try again."); this.showError("Failed to delete all versions. Please try again.");
} }
} }

View File

@@ -269,55 +269,79 @@ class NostrClient {
/** /**
* Edits a video by creating a *new event* with a brand-new d tag, * Edits a video by creating a *new event* with a brand-new d tag,
* but reuses the same videoRootId as the original. * but reuses the same videoRootId as the original.
*
* => old link remains pinned to the old event, new link is a fresh ID. * => old link remains pinned to the old event, new link is a fresh ID.
* => older version is overshadowed if your dedupe logic only shows newest.
*/ */
async editVideo(originalVideo, updatedData, pubkey) { async editVideo(originalEventStub, updatedData, pubkey) {
if (!pubkey) throw new Error("Not logged in to edit."); if (!pubkey) {
if (originalVideo.pubkey !== pubkey) { throw new Error("Not logged in to edit.");
throw new Error("You do not own this video (different pubkey)."); }
if (!originalEventStub.pubkey || originalEventStub.pubkey !== pubkey) {
throw new Error("You do not own this video (pubkey mismatch).");
} }
// Use the videoRootId directly from the converted video // 1) Attempt to get the FULL old event details (especially videoRootId)
const rootId = originalVideo.videoRootId || null; let baseEvent = originalEventStub;
// If the caller didn't pass .videoRootId, fetch from local or relay:
if (!baseEvent.videoRootId) {
const fetched = await this.getEventById(originalEventStub.id);
if (!fetched) {
throw new Error("Could not retrieve the original event to edit.");
}
baseEvent = fetched;
}
// 2) We now have baseEvent.videoRootId if it existed
let oldRootId = baseEvent.videoRootId || null;
// Decrypt the old magnet if it was private // Decrypt the old magnet if it was private
let oldPlainMagnet = originalVideo.magnet || ""; let oldPlainMagnet = baseEvent.magnet || "";
if (originalVideo.isPrivate && oldPlainMagnet) { if (baseEvent.isPrivate && oldPlainMagnet) {
oldPlainMagnet = fakeDecrypt(oldPlainMagnet); oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
} }
// Determine new privacy setting // 3) Decide new privacy
const wantPrivate = const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
updatedData.isPrivate ?? originalVideo.isPrivate ?? false;
// Fallback to old magnet if none provided // 4) Fallback to old magnet if none was provided
let finalPlainMagnet = (updatedData.magnet || "").trim(); let finalPlainMagnet = (updatedData.magnet || "").trim();
if (!finalPlainMagnet) { if (!finalPlainMagnet) {
finalPlainMagnet = oldPlainMagnet; finalPlainMagnet = oldPlainMagnet;
} }
// Re-encrypt if user wants private // 5) Re-encrypt if user wants private
let finalMagnet = finalPlainMagnet; let finalMagnet = finalPlainMagnet;
if (wantPrivate) { if (wantPrivate) {
finalMagnet = fakeEncrypt(finalPlainMagnet); finalMagnet = fakeEncrypt(finalPlainMagnet);
} }
// If there's no root yet (legacy), generate it // 6) If there's no root yet (legacy), use the old event's own ID.
const newRootId = // Otherwise keep the existing rootId.
rootId || `${Date.now()}-${Math.random().toString(36).slice(2)}`; if (!oldRootId) {
oldRootId = baseEvent.id;
if (isDevMode) {
console.log(
"No existing root => using baseEvent.id as root:",
oldRootId
);
}
}
// Generate a brand-new d-tag so it doesn't overshadow the old share link
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`; const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
// Build updated content // 7) Build updated content
const contentObject = { const contentObject = {
videoRootId: newRootId, videoRootId: oldRootId,
version: updatedData.version ?? originalVideo.version ?? 1, version: updatedData.version ?? baseEvent.version ?? 1,
deleted: false, deleted: false,
isPrivate: wantPrivate, isPrivate: wantPrivate,
title: updatedData.title ?? originalVideo.title, title: updatedData.title ?? baseEvent.title,
magnet: finalMagnet, magnet: finalMagnet,
thumbnail: updatedData.thumbnail ?? originalVideo.thumbnail, thumbnail: updatedData.thumbnail ?? baseEvent.thumbnail,
description: updatedData.description ?? originalVideo.description, description: updatedData.description ?? baseEvent.description,
mode: updatedData.mode ?? originalVideo.mode ?? "live", mode: updatedData.mode ?? baseEvent.mode ?? "live",
}; };
const event = { const event = {
@@ -332,16 +356,24 @@ class NostrClient {
}; };
if (isDevMode) { if (isDevMode) {
console.log("Creating edited event with root ID:", newRootId); console.log("Creating edited event with root ID:", oldRootId);
console.log("Event content:", event.content); console.log("Event content:", event.content);
} }
// 8) Sign and publish the new event
try { try {
const signedEvent = await window.nostr.signEvent(event); const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) {
console.log("Signed edited event:", signedEvent);
}
await Promise.all( await Promise.all(
this.relays.map(async (url) => { this.relays.map(async (url) => {
try { try {
await this.pool.publish([url], signedEvent); await this.pool.publish([url], signedEvent);
if (isDevMode) {
console.log(`Edited video published to ${url}`);
}
} catch (err) { } catch (err) {
if (isDevMode) { if (isDevMode) {
console.error(`Publish failed to ${url}`, err); console.error(`Publish failed to ${url}`, err);
@@ -357,28 +389,26 @@ class NostrClient {
} }
/** /**
* "Deleting" => we just mark content as {deleted:true} and blank out magnet/desc * "Reverting" => we just mark the most recent content as {deleted:true} and blank out magnet/desc
*/ */
async deleteVideo(originalEvent, pubkey) { async revertVideo(originalEvent, pubkey) {
if (!pubkey) { if (!pubkey) {
throw new Error("Not logged in to delete."); throw new Error("Not logged in to revert.");
} }
if (originalEvent.pubkey !== pubkey) { if (originalEvent.pubkey !== pubkey) {
throw new Error("Not your event (pubkey mismatch)."); throw new Error("Not your event (pubkey mismatch).");
} }
// If front-end didn't pass the tags array, load the full event from local or from the relay: // If front-end didn't pass the tags array, load the full event:
let baseEvent = originalEvent; let baseEvent = originalEvent;
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) { if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
const fetched = await this.getEventById(originalEvent.id); const fetched = await this.getEventById(originalEvent.id);
if (!fetched) { if (!fetched) {
throw new Error("Could not fetch the original event for deletion."); throw new Error("Could not fetch the original event for reverting.");
} }
// Rebuild baseEvent as a raw Nostr event that includes .tags and .content
baseEvent = { baseEvent = {
id: fetched.id, id: fetched.id,
pubkey: fetched.pubkey, pubkey: fetched.pubkey,
// put the raw JSON content back into string form:
content: JSON.stringify({ content: JSON.stringify({
version: fetched.version, version: fetched.version,
deleted: fetched.deleted, deleted: fetched.deleted,
@@ -393,41 +423,34 @@ class NostrClient {
}; };
} }
// Now try to get the old d-tag // Check d-tag
const dTag = baseEvent.tags.find((t) => t[0] === "d"); const dTag = baseEvent.tags.find((t) => t[0] === "d");
if (!dTag) { if (!dTag) {
throw new Error('No "d" tag => cannot delete addressable kind=30078.'); throw new Error(
'No "d" tag => cannot revert addressable kind=30078 event.'
);
} }
const existingD = dTag[1]; const existingD = dTag[1];
// After you've parsed oldContent:
const oldContent = JSON.parse(baseEvent.content || "{}"); const oldContent = JSON.parse(baseEvent.content || "{}");
const oldVersion = oldContent.version ?? 1; const oldVersion = oldContent.version ?? 1;
// ADD this block to handle the old root or fallback: // If no root, fallback
let finalRootId = oldContent.videoRootId || null; let finalRootId = oldContent.videoRootId || null;
if (!finalRootId) { if (!finalRootId) {
// If its a legacy video (no root), we can fallback to your finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
// existing logic used by getActiveKey. For instance, if it had a 'd' tag:
if (dTag) {
// Some devs store it as 'LEGACY:pubkey:dTagValue'
// or you could just store the same as the old approach:
finalRootId = `LEGACY:${baseEvent.pubkey}:${dTag[1]}`;
} else {
finalRootId = `LEGACY:${baseEvent.id}`;
}
} }
// Now build the content object, including videoRootId: // Build “deleted: true” overshadow event => revert current version
const contentObject = { const contentObject = {
videoRootId: finalRootId, // <-- CRUCIAL so the delete event shares the same root key videoRootId: finalRootId,
version: oldVersion, version: oldVersion,
deleted: true, deleted: true, // mark *this version* as deleted
isPrivate: oldContent.isPrivate ?? false, isPrivate: oldContent.isPrivate ?? false,
title: oldContent.title || "", title: oldContent.title || "",
magnet: "", magnet: "",
thumbnail: "", thumbnail: "",
description: "Video was deleted by creator.", description: "This version was reverted by the creator.",
mode: oldContent.mode || "live", mode: oldContent.mode || "live",
}; };
@@ -437,44 +460,80 @@ class NostrClient {
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
["t", "video"], ["t", "video"],
// We reuse the same d => overshadow the original event ["d", existingD], // re-use same d => overshadow
["d", existingD],
], ],
content: JSON.stringify(contentObject), content: JSON.stringify(contentObject),
}; };
if (isDevMode) { const signedEvent = await window.nostr.signEvent(event);
console.log("Deleting video => mark 'deleted:true'.", event.content); await Promise.all(
} this.relays.map(async (url) => {
try {
try { await this.pool.publish([url], signedEvent);
const signedEvent = await window.nostr.signEvent(event); } catch (err) {
if (isDevMode) { if (isDevMode) {
console.log("Signed deleted event:", signedEvent); console.error(`Failed to revert on ${url}`, err);
}
// Publish everywhere
await Promise.all(
this.relays.map(async (url) => {
try {
await this.pool.publish([url], signedEvent);
if (isDevMode) {
console.log(`Delete event published to ${url}`);
}
} catch (err) {
if (isDevMode) {
console.error(`Failed to publish deleted event to ${url}:`, err);
}
} }
}) }
); })
return signedEvent; );
} catch (err) {
if (isDevMode) { return signedEvent;
console.error("Failed to sign deleted event:", err); }
}
throw new Error("Failed to sign deleted event."); /**
* "Deleting" => we just mark all content with the same videoRootId as {deleted:true} and blank out magnet/desc
*/
async deleteAllVersions(videoRootId, pubkey) {
if (!pubkey) {
throw new Error("Not logged in to delete all versions.");
} }
// 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);
}
}
// If you want to re-check the relay for older versions too,
// you can do a fallback query, but typically your local cache is enough.
if (!matchingEvents.length) {
throw new Error("No existing events found for that root.");
}
// 2) For each event, create a "deleted: true" overshadow
// by re-using the same d-tag so it cannot appear again.
for (const vid of matchingEvents) {
await this.revertVideo(
{
// re-using revertVideo logic
id: vid.id,
pubkey: vid.pubkey,
content: JSON.stringify({
version: vid.version,
deleted: vid.deleted,
isPrivate: vid.isPrivate,
title: vid.title,
magnet: vid.magnet,
thumbnail: vid.thumbnail,
description: vid.description,
mode: vid.mode,
}),
tags: vid.tags,
},
pubkey
);
}
// Optionally return some status
return true;
} }
/** /**