From 71e95c395e0940c5767e5a8a99208b72138dc456 Mon Sep 17 00:00:00 2001 From: Keep Creating Online <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:06:26 -0500 Subject: [PATCH] update --- src/about.html | 24 +- src/content/about.md | 6 - src/content/ipns.md | 19 + src/getting-started.html | 24 +- src/index.html | 154 +++- src/ipns.html | 192 +++++ src/js/app.js | 1540 ++++++++++++++++++++------------------ src/js/disclaimer.js | 29 + 8 files changed, 1222 insertions(+), 766 deletions(-) create mode 100644 src/content/ipns.md create mode 100644 src/ipns.html create mode 100644 src/js/disclaimer.js diff --git a/src/about.html b/src/about.html index 7c4a450..327bd3d 100644 --- a/src/about.html +++ b/src/about.html @@ -69,6 +69,7 @@ + diff --git a/src/content/about.md b/src/content/about.md index 05c0486..fcae05c 100644 --- a/src/content/about.md +++ b/src/content/about.md @@ -1,11 +1,5 @@ ![](https://bitvid.netlify.app/assets/jpg/bitvid.jpg) -BTC DNS: [bitvid.btc.us](https://bitvid.btc.us) - -ETH DNS: [bitvid.eth.limo](https://bitvid.eth.limo) - -**_IPNS: k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1_** - # About bitvid Welcome to bitvid, a new kind of video platform that puts you in control. Unlike traditional video sites that keep your content on their servers, bitvid lets videos flow directly between creators and viewers. Think of it like a digital potluck where everyone brings and shares content directly with each other! diff --git a/src/content/ipns.md b/src/content/ipns.md new file mode 100644 index 0000000..036d4e6 --- /dev/null +++ b/src/content/ipns.md @@ -0,0 +1,19 @@ +# IPNS Gateways + +Below is a list of available IPNS gateways you can use with the hash `k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1`: + +1. **[FLK IPFS Gateway](https://k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1.ipns.flk-ipfs.xyz/)** + A public gateway that resolves the provided IPNS hash. + +2. **[Aragon IPFS Gateway](https://ipfs.eth.aragon.network/ipfs/bafybeih2ebj55ki3wvasj5i3rhwgjn6e72f6vxsrlrjfqvzezot2eoeqz4/)** + A gateway hosted by Aragon for IPFS content resolution. + +3. **[Dweb.link Gateway](https://dweb.link/ipns/k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1)** + A subdomain resolution gateway provided by Protocol Labs. + +4. **[IPFS.io Gateway](https://ipfs.io/ipns/k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1)** + A public gateway operated by Protocol Labs. + +--- + +**Note:** The availability and performance of these gateways may vary. diff --git a/src/getting-started.html b/src/getting-started.html index e975fa4..6d05f08 100644 --- a/src/getting-started.html +++ b/src/getting-started.html @@ -69,6 +69,7 @@ + diff --git a/src/index.html b/src/index.html index a5d2544..9099cdb 100644 --- a/src/index.html +++ b/src/index.html @@ -198,7 +198,9 @@
-
Initializing...
+
+ Initializing... Just give it a sec. +
- - -
@@ -268,7 +249,9 @@ class="flex items-center justify-between text-sm text-gray-400 mb-4" > just now -
Initializing...
+
+ Initializing... Just give it a sec. +
@@ -320,6 +303,108 @@ seed. zap. subscribe.
+ + +
+
+ +
+ +
+ BitVid Logo +
+ +

+ Welcome to bitvid +

+ + +
+ + + +

+ This platform is still in development. You may encounter bugs or + missing features. +

+
+ + +
+

+ bitvid is a decentralized video platform where content is shared + directly between users. We want you to understand a few + important points before you continue: +

+ +
+
+

+ Content Responsibility +

+

+ We don't host or control any videos shared on bitvid. All + content is the responsibility of the creators who share it. + Please follow your local laws and use the platform + responsibly. +

+
+ +
+

Platform Status

+

+ bitvid is a work in progress. Features may change or break, + and security improvements are ongoing. Your feedback and + patience help us build a better platform. +

+
+ +
+

Get Involved

+

+ Are you a developer? We'd love your help! Visit our GitHub + repository to contribute to building the future of + decentralized video sharing. +

+
+
+
+ + +
+ +
+
+
+
+ + diff --git a/src/ipns.html b/src/ipns.html new file mode 100644 index 0000000..d378797 --- /dev/null +++ b/src/ipns.html @@ -0,0 +1,192 @@ + + + + + + bitvid | About + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/js/app.js b/src/js/app.js index e719f1a..0ac86bc 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,467 +1,481 @@ // js/app.js -import { nostrClient } from './nostr.js'; -import { torrentClient } from './webtorrent.js'; -import { isDevMode } from './config.js'; +import { nostrClient } from "./nostr.js"; +import { torrentClient } from "./webtorrent.js"; +import { isDevMode } from "./config.js"; +import { disclaimerModal } from "./disclaimer.js"; class bitvidApp { - constructor() { - // Authentication Elements - this.loginButton = document.getElementById('loginButton'); - this.logoutButton = document.getElementById('logoutButton'); - this.userStatus = document.getElementById('userStatus'); - this.userPubKey = document.getElementById('userPubKey'); + constructor() { + // Authentication Elements + this.loginButton = document.getElementById("loginButton"); + this.logoutButton = document.getElementById("logoutButton"); + this.userStatus = document.getElementById("userStatus"); + this.userPubKey = document.getElementById("userPubKey"); - // Form Elements - this.submitForm = document.getElementById('submitForm'); - this.videoFormContainer = document.getElementById('videoFormContainer'); + // Form Elements + this.submitForm = document.getElementById("submitForm"); + this.videoFormContainer = document.getElementById("videoFormContainer"); - // Video List Element - this.videoList = document.getElementById('videoList'); + // Video List Element + this.videoList = document.getElementById("videoList"); - // Video Player Elements - this.playerSection = document.getElementById('playerSection'); - this.videoElement = document.getElementById('video'); - this.status = document.getElementById('status'); - this.progressBar = document.getElementById('progress'); - this.peers = document.getElementById('peers'); - this.speed = document.getElementById('speed'); - this.downloaded = document.getElementById('downloaded'); + // Video Player Elements + this.playerSection = document.getElementById("playerSection"); + this.videoElement = document.getElementById("video"); + this.status = document.getElementById("status"); + this.progressBar = document.getElementById("progress"); + this.peers = document.getElementById("peers"); + this.speed = document.getElementById("speed"); + this.downloaded = document.getElementById("downloaded"); - // Modal Elements - this.playerModal = document.getElementById('playerModal'); - this.modalVideo = document.getElementById('modalVideo'); - this.modalStatus = document.getElementById('modalStatus'); - this.modalProgress = document.getElementById('modalProgress'); - this.modalPeers = document.getElementById('modalPeers'); - this.modalSpeed = document.getElementById('modalSpeed'); - this.modalDownloaded = document.getElementById('modalDownloaded'); - this.closePlayerBtn = document.getElementById('closePlayer'); + // Modal Elements + this.playerModal = document.getElementById("playerModal"); + this.modalVideo = document.getElementById("modalVideo"); + this.modalStatus = document.getElementById("modalStatus"); + this.modalProgress = document.getElementById("modalProgress"); + this.modalPeers = document.getElementById("modalPeers"); + this.modalSpeed = document.getElementById("modalSpeed"); + this.modalDownloaded = document.getElementById("modalDownloaded"); + this.closePlayerBtn = document.getElementById("closePlayer"); - // Video Info Elements - this.videoTitle = document.getElementById('videoTitle'); - this.videoDescription = document.getElementById('videoDescription'); - this.videoTimestamp = document.getElementById('videoTimestamp'); + // Video Info Elements + this.videoTitle = document.getElementById("videoTitle"); + this.videoDescription = document.getElementById("videoDescription"); + this.videoTimestamp = document.getElementById("videoTimestamp"); - // Creator Info Elements - this.creatorAvatar = document.getElementById('creatorAvatar').querySelector('img'); - this.creatorName = document.getElementById('creatorName'); - this.creatorNpub = document.getElementById('creatorNpub'); + // Creator Info Elements + this.creatorAvatar = document + .getElementById("creatorAvatar") + .querySelector("img"); + this.creatorName = document.getElementById("creatorName"); + this.creatorNpub = document.getElementById("creatorNpub"); - // Notification Containers - this.errorContainer = document.getElementById('errorContainer'); - this.successContainer = document.getElementById('successContainer'); + // Notification Containers + this.errorContainer = document.getElementById("errorContainer"); + this.successContainer = document.getElementById("successContainer"); - this.pubkey = null; - this.currentMagnetUri = null; + this.pubkey = null; + this.currentMagnetUri = null; - // ADDED FOR VERSIONING/PRIVATE/DELETE: - // If you created an in your HTML form: - this.isPrivateCheckbox = document.getElementById('isPrivate'); + // ADDED FOR VERSIONING/PRIVATE/DELETE: + // If you created an in your HTML form: + this.isPrivateCheckbox = document.getElementById("isPrivate"); + } + + /** + * Initializes the application by setting up the Nostr client and loading videos. + */ + // app.js + async init() { + try { + // Hide and reset player states + this.playerSection.style.display = "none"; + this.playerModal.style.display = "none"; + this.currentMagnetUri = null; + + // Initialize Nostr and check login + await nostrClient.init(); + const savedPubKey = localStorage.getItem("userPubKey"); + if (savedPubKey) { + this.login(savedPubKey, false); + } + + this.setupEventListeners(); + disclaimerModal.show(); // Add this line here + await this.loadVideos(); + } catch (error) { + console.error("Init failed:", error); + this.showError("Failed to connect to Nostr relay"); + } + } + + /** + * Formats a timestamp into a "time ago" format. + */ + formatTimeAgo(timestamp) { + const seconds = Math.floor(Date.now() / 1000 - timestamp); + const intervals = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + }; + + for (const [unit, secondsInUnit] of Object.entries(intervals)) { + const interval = Math.floor(seconds / secondsInUnit); + if (interval >= 1) { + return `${interval} ${unit}${interval === 1 ? "" : "s"} ago`; + } } - /** - * Initializes the application by setting up the Nostr client and loading videos. - */ - // app.js - async init() { - try { - // Hide and reset player states - this.playerSection.style.display = 'none'; - this.playerModal.style.display = 'none'; - this.currentMagnetUri = null; - - // Initialize Nostr and check login - await nostrClient.init(); - const savedPubKey = localStorage.getItem('userPubKey'); - if (savedPubKey) { - this.login(savedPubKey, false); - } + return "just now"; + } - this.setupEventListeners(); - await this.loadVideos(); - } catch (error) { - console.error('Init failed:', error); - this.showError('Failed to connect to Nostr relay'); - } + /** + * Sets up event listeners for various UI interactions. + */ + setupEventListeners() { + // Login Button + this.loginButton.addEventListener("click", async () => { + try { + const pubkey = await nostrClient.login(); + this.login(pubkey, true); + } catch (error) { + this.log("Login failed:", error); + this.showError("Failed to login. Please try again."); + } + }); + + // Logout Button + this.logoutButton.addEventListener("click", () => { + this.logout(); + }); + + // Form submission + this.submitForm.addEventListener("submit", (e) => this.handleSubmit(e)); + + // Close Modal Button + if (this.closePlayerBtn) { + this.closePlayerBtn.addEventListener("click", async () => { + await this.hideModal(); + }); } - /** - * Formats a timestamp into a "time ago" format. - */ - formatTimeAgo(timestamp) { - const seconds = Math.floor((Date.now() / 1000) - timestamp); - const intervals = { - year: 31536000, - month: 2592000, - week: 604800, - day: 86400, - hour: 3600, - minute: 60 - }; - - for (const [unit, secondsInUnit] of Object.entries(intervals)) { - const interval = Math.floor(seconds / secondsInUnit); - if (interval >= 1) { - return `${interval} ${unit}${interval === 1 ? '' : 's'} ago`; - } + // Close Modal by clicking outside content + if (this.playerModal) { + this.playerModal.addEventListener("click", async (e) => { + if (e.target === this.playerModal) { + await this.hideModal(); } - - return 'just now'; + }); } - /** - * Sets up event listeners for various UI interactions. - */ - setupEventListeners() { - // Login Button - this.loginButton.addEventListener('click', async () => { - try { - const pubkey = await nostrClient.login(); - this.login(pubkey, true); - } catch (error) { - this.log('Login failed:', error); - this.showError('Failed to login. Please try again.'); - } - }); + // Video error handling + this.videoElement.addEventListener("error", (e) => { + const error = e.target.error; + this.log("Video error:", error); + if (error) { + this.showError( + `Video playback error: ${error.message || "Unknown error"}` + ); + } + }); - // Logout Button - this.logoutButton.addEventListener('click', () => { - this.logout(); - }); - - // Form submission - this.submitForm.addEventListener('submit', (e) => this.handleSubmit(e)); - - // Close Modal Button - if (this.closePlayerBtn) { - this.closePlayerBtn.addEventListener('click', async () => { - await this.hideModal(); - }); + // Detailed Modal Video Event Listeners + if (this.modalVideo) { + this.modalVideo.addEventListener("error", (e) => { + const error = e.target.error; + this.log("Modal video error:", error); + if (error) { + this.log("Error code:", error.code); + this.log("Error message:", error.message); + this.showError( + `Video playback error: ${error.message || "Unknown error"}` + ); } + }); - // Close Modal by clicking outside content - if (this.playerModal) { - this.playerModal.addEventListener('click', async (e) => { - if (e.target === this.playerModal) { - await this.hideModal(); - } - }); - } + this.modalVideo.addEventListener("loadstart", () => { + this.log("Video loadstart event fired"); + }); - // Video error handling - this.videoElement.addEventListener('error', (e) => { - const error = e.target.error; - this.log('Video error:', error); - if (error) { - this.showError(`Video playback error: ${error.message || 'Unknown error'}`); - } - }); + this.modalVideo.addEventListener("loadedmetadata", () => { + this.log("Video loadedmetadata event fired"); + }); - // Detailed Modal Video Event Listeners - if (this.modalVideo) { - this.modalVideo.addEventListener('error', (e) => { - const error = e.target.error; - this.log('Modal video error:', error); - if (error) { - this.log('Error code:', error.code); - this.log('Error message:', error.message); - this.showError(`Video playback error: ${error.message || 'Unknown error'}`); - } - }); - - this.modalVideo.addEventListener('loadstart', () => { - this.log('Video loadstart event fired'); - }); - - this.modalVideo.addEventListener('loadedmetadata', () => { - this.log('Video loadedmetadata event fired'); - }); - - this.modalVideo.addEventListener('canplay', () => { - this.log('Video canplay event fired'); - }); - } - - // Cleanup on page unload - window.addEventListener('beforeunload', async () => { - await this.cleanup(); - }); + this.modalVideo.addEventListener("canplay", () => { + this.log("Video canplay event fired"); + }); } - /** - * Handles user login. - */ - login(pubkey, saveToStorage = true) { - this.pubkey = pubkey; - this.loginButton.classList.add('hidden'); - this.logoutButton.classList.remove('hidden'); - this.userStatus.classList.remove('hidden'); - this.userPubKey.textContent = pubkey; - this.videoFormContainer.classList.remove('hidden'); - this.log(`User logged in as: ${pubkey}`); + // Cleanup on page unload + window.addEventListener("beforeunload", async () => { + await this.cleanup(); + }); + } - if (saveToStorage) { - localStorage.setItem('userPubKey', pubkey); + /** + * Handles user login. + */ + login(pubkey, saveToStorage = true) { + this.pubkey = pubkey; + this.loginButton.classList.add("hidden"); + this.logoutButton.classList.remove("hidden"); + this.userStatus.classList.remove("hidden"); + this.userPubKey.textContent = pubkey; + this.videoFormContainer.classList.remove("hidden"); + this.log(`User logged in as: ${pubkey}`); + + if (saveToStorage) { + localStorage.setItem("userPubKey", pubkey); + } + } + + /** + * Handles user logout. + */ + logout() { + nostrClient.logout(); + this.pubkey = null; + this.loginButton.classList.remove("hidden"); + this.logoutButton.classList.add("hidden"); + this.userStatus.classList.add("hidden"); + this.userPubKey.textContent = ""; + this.videoFormContainer.classList.add("hidden"); + localStorage.removeItem("userPubKey"); + this.log("User logged out."); + } + + /** + * Cleans up video player and torrents. + */ + async cleanup() { + try { + if (this.videoElement) { + this.videoElement.pause(); + this.videoElement.src = ""; + this.videoElement.load(); + } + if (this.modalVideo) { + this.modalVideo.pause(); + this.modalVideo.src = ""; + this.modalVideo.load(); + } + await torrentClient.cleanup(); + } catch (error) { + this.log("Cleanup error:", error); + } + } + + /** + * Hides the video player section. + */ + async hideVideoPlayer() { + await this.cleanup(); + this.playerSection.classList.add("hidden"); + } + + /** + * Hides the video modal. + */ + async hideModal() { + await this.cleanup(); + this.playerModal.style.display = "none"; + this.playerModal.classList.add("hidden"); + } + + /** + * Handles video submission (with version, private listing). + */ + async handleSubmit(e) { + e.preventDefault(); + + if (!this.pubkey) { + this.showError("Please login to post a video."); + return; + } + + const descriptionElement = document.getElementById("description"); + + // ADDED FOR VERSIONING/PRIVATE/DELETE: + // If you have a checkbox with id="isPrivate" in HTML + const isPrivate = this.isPrivateCheckbox + ? this.isPrivateCheckbox.checked + : false; + + const formData = { + version: 2, // We set the version to 2 for new posts + title: document.getElementById("title")?.value.trim() || "", + magnet: document.getElementById("magnet")?.value.trim() || "", + thumbnail: document.getElementById("thumbnail")?.value.trim() || "", + description: descriptionElement?.value.trim() || "", + mode: isDevMode ? "dev" : "live", + isPrivate, // new field to handle private listings + }; + + this.log("Form Data Collected:", formData); + + if (!formData.title || !formData.magnet) { + this.showError("Title and Magnet URI are required."); + return; + } + + try { + await nostrClient.publishVideo(formData, this.pubkey); + this.submitForm.reset(); + + // If the private checkbox was checked, reset it + if (this.isPrivateCheckbox) { + this.isPrivateCheckbox.checked = false; + } + + await this.loadVideos(); + this.showSuccess("Video shared successfully!"); + } catch (error) { + this.log("Failed to publish video:", error.message); + this.showError("Failed to share video. Please try again later."); + } + } + + /** + * Loads and displays videos from Nostr. + */ + async loadVideos() { + try { + const videos = await nostrClient.fetchVideos(); + this.log("Fetched videos (raw):", videos); + + if (!videos) { + this.log("No videos received"); + throw new Error("No videos received from relays"); + } + + // Convert to array if not already + const videosArray = Array.isArray(videos) ? videos : [videos]; + + // **Filter** so we only show: + // - isPrivate === false (public videos) + // - or isPrivate === true but pubkey === this.pubkey + const displayedVideos = videosArray.filter((video) => { + if (!video.isPrivate) { + // Public video => show it + return true; } - } + // Else it's private; only show if it's owned by the logged-in user + return this.pubkey && video.pubkey === this.pubkey; + }); - /** - * Handles user logout. - */ - logout() { - nostrClient.logout(); - this.pubkey = null; - this.loginButton.classList.remove('hidden'); - this.logoutButton.classList.add('hidden'); - this.userStatus.classList.add('hidden'); - this.userPubKey.textContent = ''; - this.videoFormContainer.classList.add('hidden'); - localStorage.removeItem('userPubKey'); - this.log('User logged out.'); - } - - /** - * Cleans up video player and torrents. - */ - async cleanup() { - try { - if (this.videoElement) { - this.videoElement.pause(); - this.videoElement.src = ''; - this.videoElement.load(); - } - if (this.modalVideo) { - this.modalVideo.pause(); - this.modalVideo.src = ''; - this.modalVideo.load(); - } - await torrentClient.cleanup(); - } catch (error) { - this.log('Cleanup error:', error); - } - } - - /** - * Hides the video player section. - */ - async hideVideoPlayer() { - await this.cleanup(); - this.playerSection.classList.add('hidden'); - } - - /** - * Hides the video modal. - */ - async hideModal() { - await this.cleanup(); - this.playerModal.style.display = 'none'; - this.playerModal.classList.add('hidden'); - } - - /** - * Handles video submission (with version, private listing). - */ - async handleSubmit(e) { - e.preventDefault(); - - if (!this.pubkey) { - this.showError('Please login to post a video.'); - return; - } - - const descriptionElement = document.getElementById('description'); - - // ADDED FOR VERSIONING/PRIVATE/DELETE: - // If you have a checkbox with id="isPrivate" in HTML - const isPrivate = this.isPrivateCheckbox - ? this.isPrivateCheckbox.checked - : false; - - const formData = { - version: 2, // We set the version to 2 for new posts - title: document.getElementById('title')?.value.trim() || '', - magnet: document.getElementById('magnet')?.value.trim() || '', - thumbnail: document.getElementById('thumbnail')?.value.trim() || '', - description: descriptionElement?.value.trim() || '', - mode: isDevMode ? 'dev' : 'live', - isPrivate // new field to handle private listings - }; - - this.log('Form Data Collected:', formData); - - if (!formData.title || !formData.magnet) { - this.showError('Title and Magnet URI are required.'); - return; - } - - try { - await nostrClient.publishVideo(formData, this.pubkey); - this.submitForm.reset(); - - // If the private checkbox was checked, reset it - if (this.isPrivateCheckbox) { - this.isPrivateCheckbox.checked = false; - } - - await this.loadVideos(); - this.showSuccess('Video shared successfully!'); - } catch (error) { - this.log('Failed to publish video:', error.message); - this.showError('Failed to share video. Please try again later.'); - } - } - - /** - * Loads and displays videos from Nostr. - */ - async loadVideos() { - try { - const videos = await nostrClient.fetchVideos(); - this.log('Fetched videos (raw):', videos); - - if (!videos) { - this.log('No videos received'); - throw new Error('No videos received from relays'); - } - - // Convert to array if not already - const videosArray = Array.isArray(videos) ? videos : [videos]; - - // **Filter** so we only show: - // - isPrivate === false (public videos) - // - or isPrivate === true but pubkey === this.pubkey - const displayedVideos = videosArray.filter(video => { - if (!video.isPrivate) { - // Public video => show it - return true; - } - // Else it's private; only show if it's owned by the logged-in user - return (this.pubkey && video.pubkey === this.pubkey); - }); - - if (displayedVideos.length === 0) { - this.log('No valid videos found after filtering.'); - this.videoList.innerHTML = ` + if (displayedVideos.length === 0) { + this.log("No valid videos found after filtering."); + this.videoList.innerHTML = `

No public videos available yet. Be the first to upload one!

`; - return; - } + return; + } - this.log('Processing filtered videos:', displayedVideos); + this.log("Processing filtered videos:", displayedVideos); - displayedVideos.forEach((video, index) => { - this.log(`Video ${index} details:`, { - id: video.id, - title: video.title, - magnet: video.magnet, - isPrivate: video.isPrivate, - pubkey: video.pubkey, - created_at: video.created_at - }); - }); + displayedVideos.forEach((video, index) => { + this.log(`Video ${index} details:`, { + id: video.id, + title: video.title, + magnet: video.magnet, + isPrivate: video.isPrivate, + pubkey: video.pubkey, + created_at: video.created_at, + }); + }); - // Now render only the displayedVideos - this.renderVideoList(displayedVideos); - this.log(`Rendered ${displayedVideos.length} videos successfully`); - } catch (error) { - this.log('Failed to fetch videos:', error); - this.showError('An error occurred while loading videos. Please try again later.'); - this.videoList.innerHTML = ` + // Now render only the displayedVideos + this.renderVideoList(displayedVideos); + this.log(`Rendered ${displayedVideos.length} videos successfully`); + } catch (error) { + this.log("Failed to fetch videos:", error); + this.showError( + "An error occurred while loading videos. Please try again later." + ); + this.videoList.innerHTML = `

No videos available at the moment. Please try again later.

`; - } } + } - /** - * Renders the given list of videos. If a video is private and belongs to the user, - * highlight with a special border (e.g. border-yellow-500). - */ - async renderVideoList(videos) { + /** + * Renders the given list of videos. If a video is private and belongs to the user, + * highlight with a special border (e.g. border-yellow-500). + */ + async renderVideoList(videos) { + try { + console.log("RENDER VIDEO LIST - Start", { + videosReceived: videos, + videosCount: videos ? videos.length : "N/A", + videosType: typeof videos, + }); + + if (!videos) { + console.error("NO VIDEOS RECEIVED"); + this.videoList.innerHTML = `

No videos found.

`; + return; + } + + const videoArray = Array.isArray(videos) ? videos : [videos]; + + if (videoArray.length === 0) { + console.error("VIDEO ARRAY IS EMPTY"); + this.videoList.innerHTML = `

No videos available.

`; + return; + } + + // Sort by creation date + videoArray.sort((a, b) => b.created_at - a.created_at); + + // Prepare to fetch user profiles + const userProfiles = new Map(); + const uniquePubkeys = [...new Set(videoArray.map((v) => v.pubkey))]; + + for (const pubkey of uniquePubkeys) { try { - console.log('RENDER VIDEO LIST - Start', { - videosReceived: videos, - videosCount: videos ? videos.length : 'N/A', - videosType: typeof videos + const userEvents = await nostrClient.pool.list(nostrClient.relays, [ + { + kinds: [0], + authors: [pubkey], + limit: 1, + }, + ]); + + if (userEvents[0]?.content) { + const profile = JSON.parse(userEvents[0].content); + userProfiles.set(pubkey, { + name: profile.name || profile.display_name || "Unknown", + picture: profile.picture || `https://robohash.org/${pubkey}`, }); - - if (!videos) { - console.error('NO VIDEOS RECEIVED'); - this.videoList.innerHTML = `

No videos found.

`; - return; - } - - const videoArray = Array.isArray(videos) ? videos : [videos]; - - if (videoArray.length === 0) { - console.error('VIDEO ARRAY IS EMPTY'); - this.videoList.innerHTML = `

No videos available.

`; - return; - } - - // Sort by creation date - videoArray.sort((a, b) => b.created_at - a.created_at); - - // Prepare to fetch user profiles - const userProfiles = new Map(); - const uniquePubkeys = [...new Set(videoArray.map(v => v.pubkey))]; - - for (const pubkey of uniquePubkeys) { - try { - const userEvents = await nostrClient.pool.list(nostrClient.relays, [{ - kinds: [0], - authors: [pubkey], - limit: 1 - }]); - - if (userEvents[0]?.content) { - const profile = JSON.parse(userEvents[0].content); - userProfiles.set(pubkey, { - name: profile.name || profile.display_name || 'Unknown', - picture: profile.picture || `https://robohash.org/${pubkey}` - }); - } else { - userProfiles.set(pubkey, { - name: 'Unknown', - picture: `https://robohash.org/${pubkey}` - }); - } - } catch (error) { - console.error(`Profile fetch error for ${pubkey}:`, error); - userProfiles.set(pubkey, { - name: 'Unknown', - picture: `https://robohash.org/${pubkey}` - }); - } - } - - // Build HTML for each video - const renderedVideos = videoArray.map((video, index) => { - try { - if (!this.validateVideo(video, index)) { - console.error(`Invalid video: ${video.title}`); - return ''; - } - - const profile = userProfiles.get(video.pubkey) || { - name: 'Unknown', - picture: `https://robohash.org/${video.pubkey}` - }; - const timeAgo = this.formatTimeAgo(video.created_at); - - // If user is the owner - const canEdit = (video.pubkey === this.pubkey); + } else { + userProfiles.set(pubkey, { + name: "Unknown", + picture: `https://robohash.org/${pubkey}`, + }); + } + } catch (error) { + console.error(`Profile fetch error for ${pubkey}:`, error); + userProfiles.set(pubkey, { + name: "Unknown", + picture: `https://robohash.org/${pubkey}`, + }); + } + } - // If it's private + user owns it => highlight with a special border - const highlightClass = (video.isPrivate && canEdit) - ? 'border-2 border-yellow-500' - : 'border-none'; // normal case + // Build HTML for each video + const renderedVideos = videoArray + .map((video, index) => { + try { + if (!this.validateVideo(video, index)) { + console.error(`Invalid video: ${video.title}`); + return ""; + } - // Gear menu (unchanged) - const gearMenu = canEdit - ? ` + const profile = userProfiles.get(video.pubkey) || { + name: "Unknown", + picture: `https://robohash.org/${video.pubkey}`, + }; + const timeAgo = this.formatTimeAgo(video.created_at); + + // If user is the owner + const canEdit = video.pubkey === this.pubkey; + + // If it's private + user owns it => highlight with a special border + const highlightClass = + video.isPrivate && canEdit + ? "border-2 border-yellow-500" + : "border-none"; // normal case + + // Gear menu (unchanged) + const gearMenu = canEdit + ? `
` - : ''; - - return ` + : ""; + + return `
${ video.thumbnail - ? `${this.escapeHTML(video.title)}` - : `
+ : `
@@ -530,7 +546,9 @@ class bitvidApp {

${this.escapeHTML(video.title)}

@@ -541,7 +559,9 @@ class bitvidApp {
${profile.name} @@ -561,335 +581,387 @@ class bitvidApp {
`; - } catch (error) { - console.error(`Error processing video ${index}:`, error); - return ''; - } - }).filter(html => html.length > 0); - - console.log('Rendered videos:', renderedVideos.length); - - if (renderedVideos.length === 0) { - this.videoList.innerHTML = `

No valid videos to display.

`; - return; - } - - this.videoList.innerHTML = renderedVideos.join(''); - console.log('Videos rendered successfully'); - - } catch (error) { - console.error('Rendering error:', error); - this.videoList.innerHTML = `

Error loading videos.

`; - } - } + } catch (error) { + console.error(`Error processing video ${index}:`, error); + return ""; + } + }) + .filter((html) => html.length > 0); - /** - * Validates a video object - */ - validateVideo(video, index) { - const validationResults = { - hasId: Boolean(video?.id), - isValidId: typeof video?.id === 'string' && video.id.trim().length > 0, - hasVideo: Boolean(video), - hasTitle: Boolean(video?.title), - hasMagnet: Boolean(video?.magnet), - hasMode: Boolean(video?.mode), - hasPubkey: Boolean(video?.pubkey), - isValidTitle: typeof video?.title === 'string' && video.title.length > 0, - isValidMagnet: typeof video?.magnet === 'string' && video.magnet.length > 0, - isValidMode: typeof video?.mode === 'string' && ['dev', 'live'].includes(video.mode) - }; - - const passed = Object.values(validationResults).every(Boolean); - console.log(`Video ${video?.title} validation results:`, validationResults, passed ? 'PASSED' : 'FAILED'); - - return passed; + console.log("Rendered videos:", renderedVideos.length); + + if (renderedVideos.length === 0) { + this.videoList.innerHTML = `

No valid videos to display.

`; + return; + } + + this.videoList.innerHTML = renderedVideos.join(""); + console.log("Videos rendered successfully"); + } catch (error) { + console.error("Rendering error:", error); + this.videoList.innerHTML = `

Error loading videos.

`; } + } - /** - * Gets a user-friendly error message. - */ - getErrorMessage(error) { - if (error.message.includes('404')) { - return 'Service worker not found. Please check server configuration.'; - } else if (error.message.includes('Brave')) { - return 'Please disable Brave Shields for this site to play videos.'; - } else if (error.message.includes('timeout')) { - return 'Connection timeout. Please check your internet connection.'; - } else { - return 'Failed to play video. Please try again.'; - } + /** + * Validates a video object + */ + validateVideo(video, index) { + const validationResults = { + hasId: Boolean(video?.id), + isValidId: typeof video?.id === "string" && video.id.trim().length > 0, + hasVideo: Boolean(video), + hasTitle: Boolean(video?.title), + hasMagnet: Boolean(video?.magnet), + hasMode: Boolean(video?.mode), + hasPubkey: Boolean(video?.pubkey), + isValidTitle: typeof video?.title === "string" && video.title.length > 0, + isValidMagnet: + typeof video?.magnet === "string" && video.magnet.length > 0, + isValidMode: + typeof video?.mode === "string" && ["dev", "live"].includes(video.mode), + }; + + const passed = Object.values(validationResults).every(Boolean); + console.log( + `Video ${video?.title} validation results:`, + validationResults, + passed ? "PASSED" : "FAILED" + ); + + return passed; + } + + /** + * Gets a user-friendly error message. + */ + getErrorMessage(error) { + if (error.message.includes("404")) { + return "Service worker not found. Please check server configuration."; + } else if (error.message.includes("Brave")) { + return "Please disable Brave Shields for this site to play videos."; + } else if (error.message.includes("timeout")) { + return "Connection timeout. Please check your internet connection."; + } else { + return "Failed to play video. Please try again."; } + } - /** - * Shows an error message to the user. - */ - showError(message) { - if (this.errorContainer) { - this.errorContainer.textContent = message; - this.errorContainer.classList.remove('hidden'); - setTimeout(() => { - this.errorContainer.classList.add('hidden'); - this.errorContainer.textContent = ''; - }, 5000); - } else { - alert(message); - } + /** + * Shows an error message to the user. + */ + showError(message) { + if (this.errorContainer) { + this.errorContainer.textContent = message; + this.errorContainer.classList.remove("hidden"); + setTimeout(() => { + this.errorContainer.classList.add("hidden"); + this.errorContainer.textContent = ""; + }, 5000); + } else { + alert(message); } + } - /** - * Shows a success message to the user. - */ - showSuccess(message) { - if (this.successContainer) { - this.successContainer.textContent = message; - this.successContainer.classList.remove('hidden'); - setTimeout(() => { - this.successContainer.classList.add('hidden'); - this.successContainer.textContent = ''; - }, 5000); - } else { - alert(message); - } + /** + * Shows a success message to the user. + */ + showSuccess(message) { + if (this.successContainer) { + this.successContainer.textContent = message; + this.successContainer.classList.remove("hidden"); + setTimeout(() => { + this.successContainer.classList.add("hidden"); + this.successContainer.textContent = ""; + }, 5000); + } else { + alert(message); } + } - /** - * Escapes HTML to prevent XSS. - */ - escapeHTML(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } + /** + * Escapes HTML to prevent XSS. + */ + escapeHTML(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } - /** - * Logs messages to console. - */ - log(message) { - console.log(message); - } + /** + * Logs messages to console. + */ + log(message) { + console.log(message); + } - /** - * Plays a video given its magnet URI. - * This method handles the logic to initiate torrent download and play the video. - */ - async playVideo(magnetURI) { - try { - if (!magnetURI) { - this.showError('Invalid Magnet URI.'); - return; - } - - const decodedMagnet = decodeURIComponent(magnetURI); - - if (this.currentMagnetUri === decodedMagnet) { - this.log('Same video requested - already playing'); - return; - } - this.currentMagnetUri = decodedMagnet; - - this.playerModal.style.display = 'flex'; - this.playerModal.classList.remove('hidden'); - - // Re-fetch the latest from relays - const videos = await nostrClient.fetchVideos(); - const video = videos.find(v => v.magnet === decodedMagnet); - - if (!video) { - this.showError('Video data not found.'); - return; - } - - // Decrypt only once if user owns it - if (video.isPrivate && video.pubkey === this.pubkey && !video.alreadyDecrypted) { - this.log('User owns a private video => decrypting magnet link...'); - video.magnet = fakeDecrypt(video.magnet); - // Mark it so we don't do it again - video.alreadyDecrypted = true; - } - - const finalMagnet = video.magnet; - - // Profile fetch - let creatorProfile = { name: 'Unknown', picture: `https://robohash.org/${video.pubkey}` }; - try { - const userEvents = await nostrClient.pool.list(nostrClient.relays, [{ - kinds: [0], - authors: [video.pubkey], - limit: 1 - }]); - - // Ensure userEvents isn't empty before accessing [0] - if (userEvents.length > 0 && userEvents[0]?.content) { - const profile = JSON.parse(userEvents[0].content); - creatorProfile = { - name: profile.name || profile.display_name || 'Unknown', - picture: profile.picture || `https://robohash.org/${video.pubkey}` - }; - } - } catch (error) { - this.log('Error fetching creator profile:', error); - } - - let creatorNpub = 'Unknown'; - try { - creatorNpub = window.NostrTools.nip19.npubEncode(video.pubkey); - } catch (error) { - this.log('Error converting pubkey to npub:', error); - creatorNpub = video.pubkey; - } - - this.videoTitle.textContent = video.title || 'Untitled'; - this.videoDescription.textContent = video.description || 'No description available.'; - this.videoTimestamp.textContent = this.formatTimeAgo(video.created_at); - - this.creatorName.textContent = creatorProfile.name; - this.creatorNpub.textContent = `${creatorNpub.slice(0, 8)}...${creatorNpub.slice(-4)}`; - this.creatorAvatar.src = creatorProfile.picture; - this.creatorAvatar.alt = creatorProfile.name; - - this.log('Starting video stream with:', finalMagnet); - await torrentClient.streamVideo(finalMagnet, this.modalVideo); - - const updateInterval = setInterval(() => { - if (!document.body.contains(this.modalVideo)) { - clearInterval(updateInterval); - return; - } - - const status = document.getElementById('status'); - const progress = document.getElementById('progress'); - const peers = document.getElementById('peers'); - const speed = document.getElementById('speed'); - const downloaded = document.getElementById('downloaded'); - - if (status) this.modalStatus.textContent = status.textContent; - if (progress) this.modalProgress.style.width = progress.style.width; - if (peers) this.modalPeers.textContent = peers.textContent; - if (speed) this.modalSpeed.textContent = speed.textContent; - if (downloaded) this.modalDownloaded.textContent = downloaded.textContent; - }, 1000); - - } catch (error) { - this.log('Error in playVideo:', error); - this.showError(`Playback error: ${error.message}`); + /** + * Plays a video given its magnet URI. + * This method handles the logic to initiate torrent download and play the video. + */ + async playVideo(magnetURI) { + try { + if (!magnetURI) { + this.showError("Invalid Magnet URI."); + return; + } + + const decodedMagnet = decodeURIComponent(magnetURI); + + if (this.currentMagnetUri === decodedMagnet) { + this.log("Same video requested - already playing"); + return; + } + this.currentMagnetUri = decodedMagnet; + + this.playerModal.style.display = "flex"; + this.playerModal.classList.remove("hidden"); + + // Re-fetch the latest from relays + const videos = await nostrClient.fetchVideos(); + const video = videos.find((v) => v.magnet === decodedMagnet); + + if (!video) { + this.showError("Video data not found."); + return; + } + + // Decrypt only once if user owns it + if ( + video.isPrivate && + video.pubkey === this.pubkey && + !video.alreadyDecrypted + ) { + this.log("User owns a private video => decrypting magnet link..."); + video.magnet = fakeDecrypt(video.magnet); + // Mark it so we don't do it again + video.alreadyDecrypted = true; + } + + const finalMagnet = video.magnet; + + // Profile fetch + let creatorProfile = { + name: "Unknown", + picture: `https://robohash.org/${video.pubkey}`, + }; + try { + const userEvents = await nostrClient.pool.list(nostrClient.relays, [ + { + kinds: [0], + authors: [video.pubkey], + limit: 1, + }, + ]); + + // Ensure userEvents isn't empty before accessing [0] + if (userEvents.length > 0 && userEvents[0]?.content) { + const profile = JSON.parse(userEvents[0].content); + creatorProfile = { + name: profile.name || profile.display_name || "Unknown", + picture: profile.picture || `https://robohash.org/${video.pubkey}`, + }; } - } + } catch (error) { + this.log("Error fetching creator profile:", error); + } - updateTorrentStatus(torrent) { - if (!torrent) return; + let creatorNpub = "Unknown"; + try { + creatorNpub = window.NostrTools.nip19.npubEncode(video.pubkey); + } catch (error) { + this.log("Error converting pubkey to npub:", error); + creatorNpub = video.pubkey; + } - this.modalStatus.textContent = torrent.status; - this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`; - this.modalPeers.textContent = `Peers: ${torrent.numPeers}`; - this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed(2)} KB/s`; - this.modalDownloaded.textContent = `${(torrent.downloaded / (1024 * 1024)).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`; + this.videoTitle.textContent = video.title || "Untitled"; + this.videoDescription.textContent = + video.description || "No description available."; + this.videoTimestamp.textContent = this.formatTimeAgo(video.created_at); - if (torrent.ready) { - this.modalStatus.textContent = 'Ready to play'; - } else { - setTimeout(() => this.updateTorrentStatus(torrent), 1000); + this.creatorName.textContent = creatorProfile.name; + this.creatorNpub.textContent = `${creatorNpub.slice( + 0, + 8 + )}...${creatorNpub.slice(-4)}`; + this.creatorAvatar.src = creatorProfile.picture; + this.creatorAvatar.alt = creatorProfile.name; + + this.log("Starting video stream with:", finalMagnet); + await torrentClient.streamVideo(finalMagnet, this.modalVideo); + + const updateInterval = setInterval(() => { + if (!document.body.contains(this.modalVideo)) { + clearInterval(updateInterval); + return; } + + const status = document.getElementById("status"); + const progress = document.getElementById("progress"); + const peers = document.getElementById("peers"); + const speed = document.getElementById("speed"); + const downloaded = document.getElementById("downloaded"); + + if (status) this.modalStatus.textContent = status.textContent; + if (progress) this.modalProgress.style.width = progress.style.width; + if (peers) this.modalPeers.textContent = peers.textContent; + if (speed) this.modalSpeed.textContent = speed.textContent; + if (downloaded) + this.modalDownloaded.textContent = downloaded.textContent; + }, 1000); + } catch (error) { + this.log("Error in playVideo:", error); + this.showError(`Playback error: ${error.message}`); } + } - /** - * Allows the user to edit a video note (only if they are the owner). - * We reuse the note's existing d tag via nostrClient.editVideo. - */ - async handleEditVideo(index) { - try { - const videos = await nostrClient.fetchVideos(); - const video = videos[index]; - - if (!this.pubkey) { - this.showError('Please login to edit videos.'); - return; - } - if (video.pubkey !== this.pubkey) { - this.showError('You do not own this video.'); - return; - } - - // Prompt for new fields or keep old - const newTitle = prompt('New Title? (Leave blank to keep existing)', video.title); - const newMagnet = prompt('New Magnet? (Leave blank to keep existing)', video.magnet); - const newThumbnail = prompt('New Thumbnail? (Leave blank to keep existing)', video.thumbnail); - const newDescription = prompt('New Description? (Leave blank to keep existing)', video.description); - - // Ask user if they want the note private or public - const wantPrivate = confirm('Make this video private? OK=Yes, Cancel=No'); - - // Fallback to old if user typed nothing - const title = (newTitle === null || newTitle.trim() === '') ? video.title : newTitle.trim(); - const magnet = (newMagnet === null || newMagnet.trim() === '') ? video.magnet : newMagnet.trim(); - const thumbnail = (newThumbnail === null || newThumbnail.trim() === '') ? video.thumbnail : newThumbnail.trim(); - const description = (newDescription === null || newDescription.trim() === '') ? video.description : newDescription.trim(); - - // Build final updated data - const updatedData = { - version: video.version || 2, // keep old version or set 2 - isPrivate: wantPrivate, - title, - magnet, - thumbnail, - description, - mode: isDevMode ? 'dev' : 'live' - }; - - // Edit - const originalEvent = { - id: video.id, - pubkey: video.pubkey, - tags: video.tags - }; - await nostrClient.editVideo(originalEvent, updatedData, this.pubkey); - this.showSuccess('Video updated successfully!'); - await this.loadVideos(); - } catch (err) { - this.log('Failed to edit video:', err.message); - this.showError('Failed to edit video. Please try again later.'); - } - } + updateTorrentStatus(torrent) { + if (!torrent) return; - /** - * ADDED FOR VERSIONING/PRIVATE/DELETE: - * Allows the user to delete (soft-delete) a video by marking it as deleted. - */ - async handleDeleteVideo(index) { - try { - const videos = await nostrClient.fetchVideos(); - const video = videos[index]; + this.modalStatus.textContent = torrent.status; + this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`; + this.modalPeers.textContent = `Peers: ${torrent.numPeers}`; + this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed( + 2 + )} KB/s`; + this.modalDownloaded.textContent = `${( + torrent.downloaded / + (1024 * 1024) + ).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`; - if (!this.pubkey) { - this.showError('Please login to delete videos.'); - return; - } - if (video.pubkey !== this.pubkey) { - this.showError('You do not own this video.'); - return; - } - - if (!confirm(`Are you sure you want to delete "${video.title}"? This action cannot be undone.`)) { - return; - } - - const originalEvent = { - id: video.id, - pubkey: video.pubkey, - tags: video.tags - }; - - await nostrClient.deleteVideo(originalEvent, this.pubkey); - this.showSuccess('Video deleted (hidden) successfully!'); - await this.loadVideos(); - } catch (err) { - this.log('Failed to delete video:', err.message); - this.showError('Failed to delete video. Please try again later.'); - } + if (torrent.ready) { + this.modalStatus.textContent = "Ready to play"; + } else { + setTimeout(() => this.updateTorrentStatus(torrent), 1000); } + } + + /** + * Allows the user to edit a video note (only if they are the owner). + * We reuse the note's existing d tag via nostrClient.editVideo. + */ + async handleEditVideo(index) { + try { + const videos = await nostrClient.fetchVideos(); + const video = videos[index]; + + if (!this.pubkey) { + this.showError("Please login to edit videos."); + return; + } + if (video.pubkey !== this.pubkey) { + this.showError("You do not own this video."); + return; + } + + // Prompt for new fields or keep old + const newTitle = prompt( + "New Title? (Leave blank to keep existing)", + video.title + ); + const newMagnet = prompt( + "New Magnet? (Leave blank to keep existing)", + video.magnet + ); + const newThumbnail = prompt( + "New Thumbnail? (Leave blank to keep existing)", + video.thumbnail + ); + const newDescription = prompt( + "New Description? (Leave blank to keep existing)", + video.description + ); + + // Ask user if they want the note private or public + const wantPrivate = confirm("Make this video private? OK=Yes, Cancel=No"); + + // Fallback to old if user typed nothing + const title = + newTitle === null || newTitle.trim() === "" + ? video.title + : newTitle.trim(); + const magnet = + newMagnet === null || newMagnet.trim() === "" + ? video.magnet + : newMagnet.trim(); + const thumbnail = + newThumbnail === null || newThumbnail.trim() === "" + ? video.thumbnail + : newThumbnail.trim(); + const description = + newDescription === null || newDescription.trim() === "" + ? video.description + : newDescription.trim(); + + // Build final updated data + const updatedData = { + version: video.version || 2, // keep old version or set 2 + isPrivate: wantPrivate, + title, + magnet, + thumbnail, + description, + mode: isDevMode ? "dev" : "live", + }; + + // Edit + const originalEvent = { + id: video.id, + pubkey: video.pubkey, + tags: video.tags, + }; + await nostrClient.editVideo(originalEvent, updatedData, this.pubkey); + this.showSuccess("Video updated successfully!"); + await this.loadVideos(); + } catch (err) { + this.log("Failed to edit video:", err.message); + this.showError("Failed to edit video. Please try again later."); + } + } + + /** + * ADDED FOR VERSIONING/PRIVATE/DELETE: + * Allows the user to delete (soft-delete) a video by marking it as deleted. + */ + async handleDeleteVideo(index) { + try { + const videos = await nostrClient.fetchVideos(); + const video = videos[index]; + + if (!this.pubkey) { + this.showError("Please login to delete videos."); + return; + } + if (video.pubkey !== this.pubkey) { + this.showError("You do not own this video."); + return; + } + + if ( + !confirm( + `Are you sure you want to delete "${video.title}"? This action cannot be undone.` + ) + ) { + return; + } + + const originalEvent = { + id: video.id, + pubkey: video.pubkey, + tags: video.tags, + }; + + await nostrClient.deleteVideo(originalEvent, this.pubkey); + this.showSuccess("Video deleted (hidden) successfully!"); + await this.loadVideos(); + } catch (err) { + this.log("Failed to delete video:", err.message); + this.showError("Failed to delete video. Please try again later."); + } + } } export const app = new bitvidApp(); diff --git a/src/js/disclaimer.js b/src/js/disclaimer.js new file mode 100644 index 0000000..435ea40 --- /dev/null +++ b/src/js/disclaimer.js @@ -0,0 +1,29 @@ +class DisclaimerModal { + constructor() { + this.modal = document.getElementById("disclaimerModal"); + this.acceptButton = document.getElementById("acceptDisclaimer"); + this.hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer"); + + this.setupEventListeners(); + } + + setupEventListeners() { + const closeModal = () => { + this.modal.style.display = "none"; + document.body.style.overflow = "unset"; + localStorage.setItem("hasSeenDisclaimer", "true"); + }; + + // Only keep the accept button event listener + this.acceptButton.addEventListener("click", closeModal); + } + + show() { + if (!this.hasSeenDisclaimer) { + this.modal.style.display = "flex"; + document.body.style.overflow = "hidden"; + } + } +} + +export const disclaimerModal = new DisclaimerModal();