mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 15:08:44 +00:00
Merge pull request #4 from PR0M3TH3AN/unstable
Added better UI modal flow and reliable revert/edit/delete functionality
This commit is contained in:
105
src/components/application-form.html
Normal file
105
src/components/application-form.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Application Form</title>
|
||||||
|
<!-- Include Tailwind CSS -->
|
||||||
|
<link href="css/tailwind.min.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 text-gray-100">
|
||||||
|
<!-- Button to open the modal -->
|
||||||
|
<div class="p-4">
|
||||||
|
<button
|
||||||
|
id="openNostrForm"
|
||||||
|
class="bg-blue-500 px-4 py-2 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Open Nostr Form
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Container -->
|
||||||
|
<div
|
||||||
|
id="nostrFormModal"
|
||||||
|
class="fixed inset-0 z-50 hidden"
|
||||||
|
style="background: transparent"
|
||||||
|
>
|
||||||
|
<!-- Overlay Layer with dark background and blur -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 z-10"
|
||||||
|
style="
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal Container -->
|
||||||
|
<div
|
||||||
|
class="relative modal-container h-full w-full flex items-center justify-center overflow-y-auto z-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal-content bg-gray-900 w-full max-w-lg md:max-w-2xl my-0 rounded-lg overflow-hidden relative max-h-[90vh]"
|
||||||
|
>
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<div
|
||||||
|
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent transition-transform duration-300 p-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-bold text-white">My Nostr Form</h2>
|
||||||
|
<button
|
||||||
|
id="closeNostrFormModal"
|
||||||
|
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6 text-gray-300"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Modal Content -->
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<iframe
|
||||||
|
src="https://formstr.app/#/f/naddr1qvzqqqr4mqpzpcf63zey4c02lz9x4zkn2ny5kmz3g8v8fj9hvehyps5tuj7u272gqythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qqsky6t5we5kgh6hdp5hgetvd9ehgh6pwpcxc6trv96xjmmwtarx7und0qc765?hideTitleImage=true"
|
||||||
|
height="700px"
|
||||||
|
width="480px"
|
||||||
|
frameborder="0"
|
||||||
|
style="
|
||||||
|
border-style: none;
|
||||||
|
box-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
"
|
||||||
|
cellspacing="0"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Script to toggle modal -->
|
||||||
|
<script>
|
||||||
|
const openBtn = document.getElementById("openNostrForm");
|
||||||
|
const modal = document.getElementById("nostrFormModal");
|
||||||
|
const closeBtn = document.getElementById("closeNostrFormModal");
|
||||||
|
|
||||||
|
openBtn.addEventListener("click", () => {
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
closeBtn.addEventListener("click", () => {
|
||||||
|
modal.classList.add("hidden");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
67
src/components/login-modal.html
Normal file
67
src/components/login-modal.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<div
|
||||||
|
id="loginModal"
|
||||||
|
class="fixed inset-0 z-50 hidden"
|
||||||
|
style="background: transparent"
|
||||||
|
>
|
||||||
|
<!-- Dark overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 z-10"
|
||||||
|
style="
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal container -->
|
||||||
|
<div
|
||||||
|
class="relative modal-container h-full w-full flex items-center justify-center overflow-y-auto z-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal-content bg-gray-900 w-full max-w-md my-0 rounded-lg overflow-hidden relative max-h-[90vh]"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-bold text-white">Login Options</h2>
|
||||||
|
<button
|
||||||
|
id="closeLoginModal"
|
||||||
|
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6 text-gray-300"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Body with login buttons -->
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<button
|
||||||
|
id="loginNIP07"
|
||||||
|
class="w-full bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Login with Extension (NIP-07)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="loginNSEC"
|
||||||
|
class="w-full bg-gray-500 text-white px-4 py-2 rounded-md cursor-not-allowed"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Login with NSEC (Coming Soon)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -1,10 +1,25 @@
|
|||||||
<!-- components/profile-modal.html -->
|
<!-- components/profile-modal.html -->
|
||||||
<div id="profileModal" class="fixed inset-0 bg-black/90 z-50 hidden">
|
<div
|
||||||
|
id="profileModal"
|
||||||
|
class="fixed inset-0 z-50 hidden"
|
||||||
|
style="background: transparent"
|
||||||
|
>
|
||||||
|
<!-- Overlay Layer with dark background and blur -->
|
||||||
<div
|
<div
|
||||||
class="modal-container h-full w-full flex items-start justify-center overflow-y-auto"
|
class="absolute inset-0 z-10"
|
||||||
|
style="
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal Container -->
|
||||||
|
<div
|
||||||
|
class="relative modal-container h-full w-full flex items-start md:items-center justify-center overflow-y-auto z-20"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-content bg-gray-900 w-full max-w-sm my-8 rounded-lg overflow-hidden relative flex flex-col"
|
class="modal-content bg-gray-900 w-full max-w-sm md:max-w-md my-0 rounded-lg overflow-hidden relative flex flex-col max-h-[90vh]"
|
||||||
>
|
>
|
||||||
<!-- Modal Header -->
|
<!-- Modal Header -->
|
||||||
<div
|
<div
|
||||||
|
@@ -1,10 +1,25 @@
|
|||||||
<!-- components/upload-modal.html -->
|
<!-- components/upload-modal.html -->
|
||||||
<div id="uploadModal" class="fixed inset-0 bg-black/90 z-50 hidden">
|
<div
|
||||||
|
id="uploadModal"
|
||||||
|
class="fixed inset-0 z-50 hidden"
|
||||||
|
style="background: transparent"
|
||||||
|
>
|
||||||
|
<!-- Overlay Layer with dark background and blur -->
|
||||||
<div
|
<div
|
||||||
class="modal-container h-full w-full flex items-start justify-center overflow-y-auto"
|
class="absolute inset-0 z-10"
|
||||||
|
style="
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal Container -->
|
||||||
|
<div
|
||||||
|
class="relative modal-container h-full w-full flex items-start md:items-center justify-center overflow-y-auto z-20"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-content bg-gray-900 w-full max-w-lg my-8 rounded-lg overflow-hidden relative"
|
class="modal-content bg-gray-900 w-full max-w-lg md:max-w-2xl my-0 rounded-lg overflow-hidden relative max-h-[90vh]"
|
||||||
>
|
>
|
||||||
<!-- Top Bar (similar to video-modal) -->
|
<!-- Top Bar (similar to video-modal) -->
|
||||||
<div
|
<div
|
||||||
|
@@ -89,12 +89,21 @@ header img {
|
|||||||
#playerModal {
|
#playerModal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: rgb(0 0 0 / 0.9);
|
background-color: rgb(0 0 0 / 0.9); /* You can adjust opacity if needed */
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
display: none; /* Hidden by default */
|
display: none; /* Hidden by default */
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px); /* For Safari support */
|
||||||
|
}
|
||||||
|
|
||||||
|
#nostrFormModal,
|
||||||
|
#profileModal,
|
||||||
|
#uploadModal {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px); /* For Safari */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If you ever want to show it, add ".flex" class dynamically */
|
/* If you ever want to show it, add ".flex" class dynamically */
|
||||||
|
101
src/index.html
101
src/index.html
@@ -38,7 +38,6 @@
|
|||||||
<link href="css/tailwind.min.css" rel="stylesheet" />
|
<link href="css/tailwind.min.css" rel="stylesheet" />
|
||||||
<link href="css/style.css" rel="stylesheet" />
|
<link href="css/style.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-100">
|
<body class="bg-gray-100">
|
||||||
<div
|
<div
|
||||||
id="app"
|
id="app"
|
||||||
@@ -59,26 +58,26 @@
|
|||||||
<button
|
<button
|
||||||
id="loginButton"
|
id="loginButton"
|
||||||
style="background-color: #fe0032"
|
style="background-color: #fe0032"
|
||||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full text-white text-sm font-bold leading-none whitespace-nowrap appearance-none hover:bg-[#e6002c] focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
|
class="inline-flex items-center justify-center w-12 h-12 rounded-full text-white text-sm font-bold leading-none hover:bg-[#e6002c] focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
|
||||||
>
|
>
|
||||||
NIP-07
|
login
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Upload (Add Video) Button -->
|
<!-- Upload Button (hidden by default) -->
|
||||||
<button
|
<button
|
||||||
id="uploadButton"
|
id="uploadButton"
|
||||||
style="background-color: #fe0032"
|
style="background-color: #fe0032"
|
||||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full text-white text-xl font-bold leading-none whitespace-nowrap appearance-none hover:bg-[#e6002c] focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 hidden"
|
class="hidden inline-flex items-center justify-center w-12 h-12 rounded-full text-white text-xl font-bold leading-none hover:bg-[#e6002c] focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Profile Button -->
|
<!-- Profile Button (hidden by default) -->
|
||||||
<button
|
<button
|
||||||
id="profileButton"
|
id="profileButton"
|
||||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-black text-white text-sm leading-none hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black hidden"
|
class="hidden inline-flex items-center justify-center w-12 h-12 rounded-full bg-black text-white text-sm leading-none hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
|
||||||
>
|
>
|
||||||
<!-- Ensures a border around the smaller avatar -->
|
<!-- This inner DIV is just an avatar container, if you like -->
|
||||||
<div class="w-10 h-10 rounded-full overflow-hidden">
|
<div class="w-10 h-10 rounded-full overflow-hidden">
|
||||||
<img
|
<img
|
||||||
id="profileAvatar"
|
id="profileAvatar"
|
||||||
@@ -95,24 +94,17 @@
|
|||||||
<div
|
<div
|
||||||
id="errorContainer"
|
id="errorContainer"
|
||||||
class="hidden bg-red-100 text-red-900 p-4 rounded-md mb-4"
|
class="hidden bg-red-100 text-red-900 p-4 rounded-md mb-4"
|
||||||
>
|
></div>
|
||||||
<!-- Error messages will appear here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Success Container -->
|
<!-- Success Container -->
|
||||||
<div
|
<div
|
||||||
id="successContainer"
|
id="successContainer"
|
||||||
class="hidden bg-green-100 text-green-900 p-4 rounded-md mb-4"
|
class="hidden bg-green-100 text-green-900 p-4 rounded-md mb-4"
|
||||||
>
|
></div>
|
||||||
<!-- Success messages will appear here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main container for dynamic views -->
|
<!-- Main container for dynamic views (most-recent-videos.html, etc.) -->
|
||||||
<main id="viewContainer" class="flex-grow mb-8">
|
<main id="viewContainer" class="flex-grow mb-8"></main>
|
||||||
<!-- We'll load "most-recent-videos.html" or other views here -->
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Imported Video Player Modal (goes into modalContainer) -->
|
<!-- Modal Container (external modals will be injected here) -->
|
||||||
<div id="modalContainer"></div>
|
<div id="modalContainer"></div>
|
||||||
|
|
||||||
<!-- Tagline / Slogan -->
|
<!-- Tagline / Slogan -->
|
||||||
@@ -122,7 +114,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Disclaimer Modal -->
|
<!-- Disclaimer Modal (inline markup; adjust as needed) -->
|
||||||
<div id="disclaimerModal" class="hidden">
|
<div id="disclaimerModal" class="hidden">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-scroll">
|
<div class="modal-scroll">
|
||||||
@@ -133,11 +125,9 @@
|
|||||||
class="h-16"
|
class="h-16"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-2xl font-bold mb-4 text-center text-white">
|
<h2 class="text-2xl font-bold mb-4 text-center text-white">
|
||||||
Welcome to bitvid
|
Welcome to bitvid
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Warning Alert -->
|
<!-- Warning Alert -->
|
||||||
<div
|
<div
|
||||||
class="bg-yellow-900/20 border border-yellow-700/50 rounded-lg p-4 mb-6 flex items-start"
|
class="bg-yellow-900/20 border border-yellow-700/50 rounded-lg p-4 mb-6 flex items-start"
|
||||||
@@ -163,14 +153,12 @@
|
|||||||
load initially.
|
load initially.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 text-gray-300">
|
<div class="space-y-6 text-gray-300">
|
||||||
<p>
|
<p>
|
||||||
bitvid is a decentralized video platform where content is shared
|
bitvid is a decentralized video platform where content is shared
|
||||||
directly between users. We want you to understand a few
|
directly between users. We want you to understand a few
|
||||||
important points before you continue:
|
important points before you continue:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="bg-gray-800 rounded-lg p-4">
|
<div class="bg-gray-800 rounded-lg p-4">
|
||||||
<h3 class="text-white font-semibold mb-2">
|
<h3 class="text-white font-semibold mb-2">
|
||||||
@@ -183,7 +171,6 @@
|
|||||||
maintain quality content during our early stages.
|
maintain quality content during our early stages.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-lg p-4">
|
<div class="bg-gray-800 rounded-lg p-4">
|
||||||
<h3 class="text-white font-semibold mb-2">
|
<h3 class="text-white font-semibold mb-2">
|
||||||
Content Responsibility & Moderation
|
Content Responsibility & Moderation
|
||||||
@@ -195,7 +182,6 @@
|
|||||||
content must follow local laws and platform guidelines.
|
content must follow local laws and platform guidelines.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-lg p-4">
|
<div class="bg-gray-800 rounded-lg p-4">
|
||||||
<h3 class="text-white font-semibold mb-2">Platform Status</h3>
|
<h3 class="text-white font-semibold mb-2">Platform Status</h3>
|
||||||
<p class="text-gray-400">
|
<p class="text-gray-400">
|
||||||
@@ -204,7 +190,6 @@
|
|||||||
patience help us build a better platform.
|
patience help us build a better platform.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-lg p-4">
|
<div class="bg-gray-800 rounded-lg p-4">
|
||||||
<h3 class="text-white font-semibold mb-2">Get Involved</h3>
|
<h3 class="text-white font-semibold mb-2">Get Involved</h3>
|
||||||
<p class="text-gray-400">
|
<p class="text-gray-400">
|
||||||
@@ -216,7 +201,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<button
|
<button
|
||||||
id="acceptDisclaimer"
|
id="acceptDisclaimer"
|
||||||
@@ -339,20 +323,51 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Scripts -->
|
|
||||||
<script src="js/libs/nostr.bundle.js"></script>
|
|
||||||
<script type="module" src="js/config.js"></script>
|
|
||||||
<script type="module" src="js/lists.js"></script>
|
|
||||||
<script type="module" src="js/accessControl.js"></script>
|
|
||||||
<script type="module" src="js/webtorrent.js"></script>
|
|
||||||
<script type="module" src="js/nostr.js"></script>
|
|
||||||
|
|
||||||
<!-- Optional: a separate manager for view loading -->
|
|
||||||
<script type="module" src="js/viewManager.js"></script>
|
|
||||||
|
|
||||||
<!-- Main app script -->
|
|
||||||
<script type="module" src="js/app.js"></script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Load external modal components -->
|
||||||
|
<script>
|
||||||
|
async function loadModal(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to load " + url);
|
||||||
|
}
|
||||||
|
const html = await response.text();
|
||||||
|
document
|
||||||
|
.getElementById("modalContainer")
|
||||||
|
.insertAdjacentHTML("beforeend", html);
|
||||||
|
console.log(url, "loaded");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just load the login modal (or any others), without adding event listeners here.
|
||||||
|
// The logic to open/close the modal is all in app.js now.
|
||||||
|
Promise.all([loadModal("components/login-modal.html")]).then(() => {
|
||||||
|
console.log("Modals loaded (login-modal.html, etc.)");
|
||||||
|
// Now that the login-modal is definitely in the DOM:
|
||||||
|
const closeBtn = document.getElementById("closeLoginModal");
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener("click", () => {
|
||||||
|
const loginModal = document.getElementById("loginModal");
|
||||||
|
if (loginModal) {
|
||||||
|
loginModal.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="js/libs/nostr.bundle.js"></script>
|
||||||
|
<script type="module" src="js/config.js"></script>
|
||||||
|
<script type="module" src="js/lists.js"></script>
|
||||||
|
<script type="module" src="js/accessControl.js"></script>
|
||||||
|
<script type="module" src="js/webtorrent.js"></script>
|
||||||
|
<script type="module" src="js/nostr.js"></script>
|
||||||
|
<script type="module" src="js/viewManager.js"></script>
|
||||||
|
<script type="module" src="js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
435
src/js/app.js
435
src/js/app.js
@@ -70,17 +70,31 @@ class bitvidApp {
|
|||||||
|
|
||||||
// Auth state
|
// Auth state
|
||||||
this.pubkey = null;
|
this.pubkey = null;
|
||||||
// Currently playing magnet
|
|
||||||
this.currentMagnetUri = null;
|
this.currentMagnetUri = null;
|
||||||
// The active video object
|
|
||||||
this.currentVideo = null;
|
this.currentVideo = null;
|
||||||
// Subscription reference (for unsubscribing)
|
|
||||||
this.videoSubscription = null;
|
this.videoSubscription = null;
|
||||||
|
|
||||||
// Videos stored as a Map (key=event.id)
|
// Videos stored as a Map (key=event.id)
|
||||||
this.videosMap = new Map();
|
this.videosMap = new Map();
|
||||||
// Simple cache for user profiles
|
// Simple cache for user profiles
|
||||||
this.profileCache = new Map();
|
this.profileCache = new Map();
|
||||||
|
|
||||||
|
// NEW: reference to the login modal's close button
|
||||||
|
this.closeLoginModalBtn =
|
||||||
|
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() {
|
||||||
@@ -107,7 +121,7 @@ class bitvidApp {
|
|||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
disclaimerModal.show();
|
disclaimerModal.show();
|
||||||
|
|
||||||
// 6. Load the default view
|
// 6. Load the default view (most-recent-videos.html)
|
||||||
await loadView("views/most-recent-videos.html");
|
await loadView("views/most-recent-videos.html");
|
||||||
|
|
||||||
// 7. Once loaded, get a reference to #videoList
|
// 7. Once loaded, get a reference to #videoList
|
||||||
@@ -119,8 +133,7 @@ class bitvidApp {
|
|||||||
// 9. Check URL ?v= param
|
// 9. Check URL ?v= param
|
||||||
this.checkUrlParams();
|
this.checkUrlParams();
|
||||||
|
|
||||||
// (Recommended) Keep an array of active interval IDs
|
// Keep an array of active interval IDs so we can clear them on modal close
|
||||||
// so we can clear them when the modal closes:
|
|
||||||
this.activeIntervals = [];
|
this.activeIntervals = [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Init failed:", error);
|
console.error("Init failed:", error);
|
||||||
@@ -143,9 +156,13 @@ class bitvidApp {
|
|||||||
if (!modalContainer) {
|
if (!modalContainer) {
|
||||||
throw new Error("Modal container element not found!");
|
throw new Error("Modal container element not found!");
|
||||||
}
|
}
|
||||||
modalContainer.innerHTML = html;
|
|
||||||
|
|
||||||
// Confirm we have a close button, etc.
|
// Instead of overwriting, we append a new DIV with the fetched HTML
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.innerHTML = html; // set the markup
|
||||||
|
modalContainer.appendChild(wrapper); // append the markup
|
||||||
|
|
||||||
|
// Now we can safely find elements inside:
|
||||||
const closeButton = document.getElementById("closeModal");
|
const closeButton = document.getElementById("closeModal");
|
||||||
if (!closeButton) {
|
if (!closeButton) {
|
||||||
throw new Error("Close button not found in video-modal!");
|
throw new Error("Close button not found in video-modal!");
|
||||||
@@ -353,29 +370,17 @@ class bitvidApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup general event listeners for login, logout, modals, etc.
|
* Setup general event listeners for logout, modals, etc.
|
||||||
*/
|
*/
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Login
|
// 1) Logout button
|
||||||
if (this.loginButton) {
|
|
||||||
this.loginButton.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
const pubkey = await nostrClient.login();
|
|
||||||
this.login(pubkey, true);
|
|
||||||
} catch (err) {
|
|
||||||
this.showError("Failed to login. Please try again.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout
|
|
||||||
if (this.logoutButton) {
|
if (this.logoutButton) {
|
||||||
this.logoutButton.addEventListener("click", () => {
|
this.logoutButton.addEventListener("click", () => {
|
||||||
this.logout();
|
this.logout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profile button (if used)
|
// 2) Profile button
|
||||||
if (this.profileButton) {
|
if (this.profileButton) {
|
||||||
this.profileButton.addEventListener("click", () => {
|
this.profileButton.addEventListener("click", () => {
|
||||||
if (this.profileModal) {
|
if (this.profileModal) {
|
||||||
@@ -384,7 +389,7 @@ class bitvidApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload button => show upload modal
|
// 3) Upload button => show upload modal
|
||||||
if (this.uploadButton) {
|
if (this.uploadButton) {
|
||||||
this.uploadButton.addEventListener("click", () => {
|
this.uploadButton.addEventListener("click", () => {
|
||||||
if (this.uploadModal) {
|
if (this.uploadModal) {
|
||||||
@@ -393,12 +398,58 @@ class bitvidApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on page unload
|
// 4) Login button => show the login modal
|
||||||
|
if (this.loginButton) {
|
||||||
|
this.loginButton.addEventListener("click", () => {
|
||||||
|
console.log("Login button clicked!");
|
||||||
|
const loginModal = document.getElementById("loginModal");
|
||||||
|
if (loginModal) {
|
||||||
|
loginModal.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Close login modal button => hide modal
|
||||||
|
if (this.closeLoginModalBtn) {
|
||||||
|
this.closeLoginModalBtn.addEventListener("click", () => {
|
||||||
|
console.log("[app.js] closeLoginModal button clicked!");
|
||||||
|
const loginModal = document.getElementById("loginModal");
|
||||||
|
if (loginModal) {
|
||||||
|
loginModal.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) NIP-07 button inside the login modal => call the extension & login
|
||||||
|
const nip07Button = document.getElementById("loginNIP07");
|
||||||
|
if (nip07Button) {
|
||||||
|
nip07Button.addEventListener("click", async () => {
|
||||||
|
console.log(
|
||||||
|
"[app.js] loginNIP07 clicked! Attempting extension login..."
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const pubkey = await nostrClient.login(); // call the extension
|
||||||
|
console.log("[NIP-07] login returned pubkey:", pubkey);
|
||||||
|
this.login(pubkey, true);
|
||||||
|
|
||||||
|
// Hide the login modal
|
||||||
|
const loginModal = document.getElementById("loginModal");
|
||||||
|
if (loginModal) {
|
||||||
|
loginModal.classList.add("hidden");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[NIP-07 login error]", err);
|
||||||
|
this.showError("Failed to login with NIP-07. Please try again.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) Cleanup on page unload
|
||||||
window.addEventListener("beforeunload", async () => {
|
window.addEventListener("beforeunload", async () => {
|
||||||
await this.cleanup();
|
await this.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle back/forward navigation => hide video modal
|
// 8) Handle back/forward nav => hide video modal
|
||||||
window.addEventListener("popstate", async () => {
|
window.addEventListener("popstate", async () => {
|
||||||
console.log("[popstate] user navigated back/forward; cleaning modal...");
|
console.log("[popstate] user navigated back/forward; cleaning modal...");
|
||||||
await this.hideModal();
|
await this.hideModal();
|
||||||
@@ -483,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) {
|
||||||
@@ -495,14 +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);
|
||||||
|
|
||||||
this.pubkey = pubkey;
|
this.pubkey = pubkey;
|
||||||
|
|
||||||
// Hide 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");
|
||||||
}
|
}
|
||||||
// We can hide logout or userStatus if we want (or they might not exist)
|
// Optionally hide logout or userStatus
|
||||||
if (this.logoutButton) {
|
if (this.logoutButton) {
|
||||||
this.logoutButton.classList.add("hidden");
|
this.logoutButton.classList.add("hidden");
|
||||||
}
|
}
|
||||||
@@ -518,24 +571,33 @@ class bitvidApp {
|
|||||||
this.profileButton.classList.remove("hidden");
|
this.profileButton.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you want to fetch your own profile to update UI
|
// (Optional) load the user's own Nostr profile
|
||||||
this.loadOwnProfile(pubkey);
|
this.loadOwnProfile(pubkey);
|
||||||
|
|
||||||
|
// 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");
|
||||||
@@ -546,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");
|
||||||
@@ -553,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -611,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">
|
||||||
@@ -627,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">
|
||||||
@@ -672,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);
|
||||||
@@ -705,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
|
||||||
@@ -808,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 },
|
||||||
@@ -846,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,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;
|
||||||
@@ -996,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)",
|
||||||
@@ -1012,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 =
|
||||||
@@ -1022,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,
|
||||||
@@ -1032,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;
|
||||||
@@ -1061,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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
219
src/js/nostr.js
219
src/js/nostr.js
@@ -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 it’s 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
1
whitelistform.txt
Normal file
1
whitelistform.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<iframe src="https://formstr.app/#/f/naddr1qvzqqqr4mqpzpkha64cj06g8sk2hqgq0yn4gqalgxxsta7x8lmvy8jr7pqfm5l9yqy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7qppvf5hganfvss9w6rfw3jkc6tnwssyzursd35kxct5d9hkugzxdaex6udmmth" height="700px" width="480px" frameborder="0" style="border-style:none;box-shadow:0px 0px 2px 2px rgba(0,0,0,0.2);" cellspacing="0" ></iframe>
|
Reference in New Issue
Block a user