From 6bb7c9047fb303f2dc7aaa630a7680c9702ef128 Mon Sep 17 00:00:00 2001 From: Keep Creating Online <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 12 Jan 2025 16:02:52 -0500 Subject: [PATCH] update --- refactoring/js/app.js | 389 +------- refactoring/js/components/Navigation.js | 0 refactoring/js/components/UserManager.js | 0 refactoring/js/components/VideoForm.js | 0 refactoring/js/components/VideoList.js | 285 ++++++ refactoring/js/config/config.js | 0 refactoring/js/models/VideoModel.js | 0 refactoring/js/nostr.js | 53 + refactoring/js/old/accessControl.js | 154 --- refactoring/js/old/app.js | 1062 --------------------- refactoring/js/old/config.js | 4 - refactoring/js/old/disclaimer.js | 29 - refactoring/js/old/lists.js | 13 - refactoring/js/old/nostr.js | 624 ------------ refactoring/js/old/webtorrent.js | 286 ------ refactoring/js/services/nostr/client.js | 0 refactoring/js/services/nostr/events.js | 0 refactoring/js/services/nostr/profiles.js | 0 refactoring/js/services/nostr/relays.js | 0 refactoring/js/services/torrent/client.js | 0 refactoring/js/services/torrent/stats.js | 0 refactoring/js/services/torrent/stream.js | 0 refactoring/js/utils/Notifications.js | 0 refactoring/js/utils/htmlUtils.js | 9 + refactoring/js/utils/logger.js | 0 refactoring/js/utils/timeUtils.js | 21 + 26 files changed, 399 insertions(+), 2530 deletions(-) delete mode 100644 refactoring/js/components/Navigation.js delete mode 100644 refactoring/js/components/UserManager.js delete mode 100644 refactoring/js/components/VideoForm.js delete mode 100644 refactoring/js/config/config.js delete mode 100644 refactoring/js/models/VideoModel.js delete mode 100644 refactoring/js/old/accessControl.js delete mode 100644 refactoring/js/old/app.js delete mode 100644 refactoring/js/old/config.js delete mode 100644 refactoring/js/old/disclaimer.js delete mode 100644 refactoring/js/old/lists.js delete mode 100644 refactoring/js/old/nostr.js delete mode 100644 refactoring/js/old/webtorrent.js delete mode 100644 refactoring/js/services/nostr/client.js delete mode 100644 refactoring/js/services/nostr/events.js delete mode 100644 refactoring/js/services/nostr/profiles.js delete mode 100644 refactoring/js/services/nostr/relays.js delete mode 100644 refactoring/js/services/torrent/client.js delete mode 100644 refactoring/js/services/torrent/stats.js delete mode 100644 refactoring/js/services/torrent/stream.js delete mode 100644 refactoring/js/utils/Notifications.js delete mode 100644 refactoring/js/utils/logger.js diff --git a/refactoring/js/app.js b/refactoring/js/app.js index 9607c89..7431f66 100644 --- a/refactoring/js/app.js +++ b/refactoring/js/app.js @@ -5,6 +5,8 @@ import { torrentClient } from "./webtorrent.js"; import { isDevMode } from "./config.js"; import { disclaimerModal } from "./disclaimer.js"; import { videoPlayer } from "./components/VideoPlayer.js"; +import { videoList } from "./components/VideoList.js"; +import { formatTimeAgo } from "./utils/timeUtils.js"; class bitvidApp { constructor() { @@ -18,9 +20,6 @@ class bitvidApp { this.submitForm = document.getElementById("submitForm"); this.videoFormContainer = document.getElementById("videoFormContainer"); - // Video List Element - this.videoList = document.getElementById("videoList"); - // Video Player Elements this.playerSection = document.getElementById("playerSection"); this.videoElement = document.getElementById("video"); @@ -64,10 +63,26 @@ class bitvidApp { this.playerSection.style.display = "none"; } - // Initialize modal first + // Initialize Nostr client first + await nostrClient.init(); + + // Handle saved pubkey + const savedPubKey = localStorage.getItem("userPubKey"); + if (savedPubKey) { + this.login(savedPubKey, false); + } + + // Initialize modal await videoPlayer.initModal(); - // Rest of your initialization code... + // Initialize video list + await videoList.loadVideos(); + + // Initialize and show disclaimer modal + disclaimerModal.show(); + + // Set up event listeners after all initializations + this.setupEventListeners(); } catch (error) { console.error("Init failed:", error); this.showError("Failed to connect to Nostr relay"); @@ -171,30 +186,6 @@ class bitvidApp { } } - /** - * 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`; - } - } - - return "just now"; - } - /** * Sets up event listeners for various UI interactions. */ @@ -290,6 +281,9 @@ class bitvidApp { this.videoFormContainer.classList.remove("hidden"); this.log(`User logged in as: ${pubkey}`); + // ADD: Update videoList pubkey + videoList.setPubkey(pubkey); + if (saveToStorage) { localStorage.setItem("userPubKey", pubkey); } @@ -393,7 +387,8 @@ class bitvidApp { this.isPrivateCheckbox.checked = false; } - await this.loadVideos(); + // CHANGE: Use videoList component to refresh + await videoList.loadVideos(); // <-- Change this line this.showSuccess("Video shared successfully!"); } catch (error) { this.log("Failed to publish video:", error.message); @@ -401,319 +396,6 @@ class bitvidApp { } } - /** - * Loads and displays videos from Nostr. - */ - async loadVideos() { - console.log("Starting loadVideos..."); - try { - const videos = await nostrClient.fetchVideos(); - console.log("Raw videos from nostrClient:", 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 = ` -
- No public videos available yet. Be the first to upload one! -
`; - return; - } - - 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, - }); - }); - - // Now render only the displayedVideos - await 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) { - 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 { - 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; - - // 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 - ? ` -- ${this.escapeHTML(profile.name)} -
-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.
`; - } - } - - /** - * 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. */ @@ -761,18 +443,6 @@ class bitvidApp { } } - /** - * Escapes HTML to prevent XSS. - */ - escapeHTML(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - /** * Logs messages to console. */ @@ -862,7 +532,7 @@ class bitvidApp { this.videoTitle.textContent = video.title || "Untitled"; this.videoDescription.textContent = video.description || "No description available."; - this.videoTimestamp.textContent = this.formatTimeAgo(video.created_at); + this.videoTimestamp.textContent = formatTimeAgo(video.created_at); this.creatorName.textContent = creatorProfile.name; this.creatorNpub.textContent = `${creatorNpub.slice( @@ -927,6 +597,7 @@ class bitvidApp { */ async handleEditVideo(index) { try { + // CHANGE: Get videos through videoList component const videos = await nostrClient.fetchVideos(); const video = videos[index]; @@ -997,7 +668,7 @@ class bitvidApp { }; await nostrClient.editVideo(originalEvent, updatedData, this.pubkey); this.showSuccess("Video updated successfully!"); - await this.loadVideos(); + await videoList.loadVideos(); } catch (err) { this.log("Failed to edit video:", err.message); this.showError("Failed to edit video. Please try again later."); @@ -1010,6 +681,7 @@ class bitvidApp { */ async handleDeleteVideo(index) { try { + // CHANGE: Get videos through videoList component const videos = await nostrClient.fetchVideos(); const video = videos[index]; @@ -1038,7 +710,8 @@ class bitvidApp { await nostrClient.deleteVideo(originalEvent, this.pubkey); this.showSuccess("Video deleted (hidden) successfully!"); - await this.loadVideos(); + // CHANGE: Use videoList component to refresh + await videoList.loadVideos(); } catch (err) { this.log("Failed to delete video:", err.message); this.showError("Failed to delete video. Please try again later."); diff --git a/refactoring/js/components/Navigation.js b/refactoring/js/components/Navigation.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/components/UserManager.js b/refactoring/js/components/UserManager.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/components/VideoForm.js b/refactoring/js/components/VideoForm.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/components/VideoList.js b/refactoring/js/components/VideoList.js index e69de29..9de08be 100644 --- a/refactoring/js/components/VideoList.js +++ b/refactoring/js/components/VideoList.js @@ -0,0 +1,285 @@ +// js/components/VideoList.js +import { nostrClient } from "../nostr.js"; +import { formatTimeAgo } from "../utils/timeUtils.js"; +import { escapeHTML } from "../utils/htmlUtils.js"; + +export class VideoList { + constructor() { + this.videoList = document.getElementById("videoList"); + this.pubkey = null; // We'll need this for private video filtering + } + + setPubkey(pubkey) { + this.pubkey = pubkey; + } + + async loadVideos() { + console.log("Starting loadVideos..."); + try { + const videos = await nostrClient.fetchVideos(); + console.log("Raw videos from nostrClient:", videos); + + if (!videos) { + console.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 private videos + const displayedVideos = videosArray.filter((video) => { + if (!video.isPrivate) { + return true; + } + return this.pubkey && video.pubkey === this.pubkey; + }); + + if (displayedVideos.length === 0) { + console.log("No valid videos found after filtering."); + this.renderEmptyState( + "No public videos available yet. Be the first to upload one!" + ); + return; + } + + console.log("Processing filtered videos:", displayedVideos); + await this.renderVideoList(displayedVideos); + console.log(`Rendered ${displayedVideos.length} videos successfully`); + } catch (error) { + console.log("Failed to fetch videos:", error); + this.renderEmptyState( + "No videos available at the moment. Please try again later." + ); + } + } + + renderEmptyState(message) { + if (this.videoList) { + this.videoList.innerHTML = ` ++ ${escapeHTML(message)} +
`; + } + } + + async renderVideoList(videos) { + try { + console.log("RENDER VIDEO LIST - Start", { + videosReceived: videos, + videosCount: videos ? videos.length : "N/A", + videosType: typeof videos, + }); + + if (!videos || videos.length === 0) { + this.renderEmptyState("No videos found."); + return; + } + + // Sort by creation date + const videoArray = [...videos].sort( + (a, b) => b.created_at - a.created_at + ); + + // Fetch user profiles + const userProfiles = await this.fetchUserProfiles(videoArray); + + // Build HTML for each video + const renderedVideos = videoArray + .map((video, index) => this.renderVideoCard(video, index, userProfiles)) + .filter(Boolean); + + if (renderedVideos.length === 0) { + this.renderEmptyState("No valid videos to display."); + return; + } + + this.videoList.innerHTML = renderedVideos.join(""); + console.log("Videos rendered successfully"); + } catch (error) { + console.error("Rendering error:", error); + this.renderEmptyState("Error loading videos."); + } + } + + async fetchUserProfiles(videos) { + const userProfiles = new Map(); + const uniquePubkeys = [...new Set(videos.map((v) => v.pubkey))]; + + for (const pubkey of uniquePubkeys) { + try { + const profile = await nostrClient.fetchUserProfile(pubkey); + userProfiles.set(pubkey, profile); + } catch (error) { + console.error(`Profile fetch error for ${pubkey}:`, error); + userProfiles.set(pubkey, { + name: "Unknown", + picture: `https://robohash.org/${pubkey}`, + }); + } + } + + return userProfiles; + } + + renderVideoCard(video, index, userProfiles) { + 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 canEdit = video.pubkey === this.pubkey; + const highlightClass = + video.isPrivate && canEdit + ? "border-2 border-yellow-500" + : "border-none"; + + return ` ++ ${escapeHTML(profile.name)} +
+- No public videos available yet. Be the first to upload one! -
`; - return; - } - - 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, - }); - }); - - // Now render only the displayedVideos - await 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) { - 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 { - 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; - - // 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 - ? ` -- ${this.escapeHTML(profile.name)} -
-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.
`; - } - } - - /** - * 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 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, "'"); - } - - /** - * 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}`); - } - } - - updateTorrentStatus(torrent) { - if (!torrent) return; - - 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 (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(); -app.init(); -window.app = app; diff --git a/refactoring/js/old/config.js b/refactoring/js/old/config.js deleted file mode 100644 index 7363993..0000000 --- a/refactoring/js/old/config.js +++ /dev/null @@ -1,4 +0,0 @@ -// js/config.js - -export const isDevMode = true; // Set to false for production -export const isWhitelistEnabled = true; // Set to false to allow all non-blacklisted users diff --git a/refactoring/js/old/disclaimer.js b/refactoring/js/old/disclaimer.js deleted file mode 100644 index 435ea40..0000000 --- a/refactoring/js/old/disclaimer.js +++ /dev/null @@ -1,29 +0,0 @@ -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(); diff --git a/refactoring/js/old/lists.js b/refactoring/js/old/lists.js deleted file mode 100644 index 51e65a0..0000000 --- a/refactoring/js/old/lists.js +++ /dev/null @@ -1,13 +0,0 @@ -// js/lists.js - -const npubs = [ - "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe", - "npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx", - "npub1j37gc05qpqzyrmdc5vetsc9h5qtstas7tr25j0n9sdpqxghz6m4q2ej6n8", - "npub1epvnvv3kskvpnmpqgnm2atevsmdferhp7dg2s0yc7uc0hdmqmgssx09tu2", -]; - -console.log("DEBUG: lists.js loaded, npubs:", npubs); - -export const initialWhitelist = npubs; -export const initialBlacklist = [""]; diff --git a/refactoring/js/old/nostr.js b/refactoring/js/old/nostr.js deleted file mode 100644 index 71b24c4..0000000 --- a/refactoring/js/old/nostr.js +++ /dev/null @@ -1,624 +0,0 @@ -// js/nostr.js - -import { isDevMode } from "./config.js"; -import { accessControl } from "./accessControl.js"; - -const RELAY_URLS = [ - "wss://relay.damus.io", - "wss://nos.lol", - "wss://relay.snort.social", - "wss://nostr.wine", -]; - -// Rate limiting for error logs -let errorLogCount = 0; -const MAX_ERROR_LOGS = 100; // Adjust as needed - -function logErrorOnce(message, eventContent = null) { - if (errorLogCount < MAX_ERROR_LOGS) { - console.error(message); - if (eventContent) { - console.log(`Event Content: ${eventContent}`); - } - errorLogCount++; - } - if (errorLogCount === MAX_ERROR_LOGS) { - console.error( - "Maximum error log limit reached. Further errors will be suppressed." - ); - } -} - -/** - * A very naive "encryption" function that just reverses the string. - * In a real app, use a proper crypto library (AES-GCM, ECDH, etc.). - */ -function fakeEncrypt(magnet) { - return magnet.split("").reverse().join(""); -} -function fakeDecrypt(encrypted) { - return encrypted.split("").reverse().join(""); -} - -class NostrClient { - constructor() { - this.pool = null; - this.pubkey = null; - this.relays = RELAY_URLS; - } - - /** - * Initializes the Nostr client by connecting to relays. - */ - async init() { - if (isDevMode) console.log("Connecting to relays..."); - - try { - this.pool = new window.NostrTools.SimplePool(); - const results = await this.connectToRelays(); - const successfulRelays = results - .filter((r) => r.success) - .map((r) => r.url); - - if (successfulRelays.length === 0) throw new Error("No relays connected"); - - if (isDevMode) - console.log(`Connected to ${successfulRelays.length} relay(s)`); - } catch (err) { - console.error("Nostr init failed:", err); - throw err; - } - } - - // Helper method to handle relay connections - async connectToRelays() { - return Promise.all( - this.relays.map( - (url) => - new Promise((resolve) => { - const sub = this.pool.sub([url], [{ kinds: [0], limit: 1 }]); - const timeout = setTimeout(() => { - sub.unsub(); - resolve({ url, success: false }); - }, 5000); - - const succeed = () => { - clearTimeout(timeout); - sub.unsub(); - resolve({ url, success: true }); - }; - - sub.on("event", succeed); - sub.on("eose", succeed); - }) - ) - ); - } - - /** - * Logs in the user using a Nostr extension or by entering an NSEC key. - */ - async login() { - try { - if (!window.nostr) { - console.log("No Nostr extension found"); - throw new Error( - "Please install a Nostr extension (like Alby or nos2x)." - ); - } - - const pubkey = await window.nostr.getPublicKey(); - const npub = window.NostrTools.nip19.npubEncode(pubkey); - - // Debug logs - if (isDevMode) { - console.log("Got pubkey:", pubkey); - console.log("Converted to npub:", npub); - console.log("Whitelist:", accessControl.getWhitelist()); - console.log("Blacklist:", accessControl.getBlacklist()); - console.log("Is whitelisted?", accessControl.isWhitelisted(npub)); - console.log("Is blacklisted?", accessControl.isBlacklisted(npub)); - } - - // Check access control - if (!accessControl.canAccess(npub)) { - if (accessControl.isBlacklisted(npub)) { - throw new Error( - "Your account has been blocked from accessing this platform." - ); - } else { - throw new Error( - "Access is currently restricted to whitelisted users only." - ); - } - } - - this.pubkey = pubkey; - if (isDevMode) - console.log( - "Successfully logged in with extension. Public key:", - this.pubkey - ); - return this.pubkey; - } catch (e) { - console.error("Login error:", e); - throw e; - } - } - - /** - * Logs out the user. - */ - logout() { - this.pubkey = null; - if (isDevMode) console.log("User logged out."); - } - - /** - * Decodes an NSEC key. - */ - decodeNsec(nsec) { - try { - const { data } = window.NostrTools.nip19.decode(nsec); - return data; - } catch (error) { - throw new Error("Invalid NSEC key."); - } - } - - /** - * Publishes a new video event to all relays (creates a brand-new note). - */ - async publishVideo(videoData, pubkey) { - if (!pubkey) { - throw new Error("User is not logged in."); - } - - if (isDevMode) { - console.log("Publishing video with data:", videoData); - } - - // If user sets "isPrivate = true", encrypt the magnet - let finalMagnet = videoData.magnet; - if (videoData.isPrivate === true) { - finalMagnet = fakeEncrypt(finalMagnet); - } - - // Default version is 1 if not specified - const version = videoData.version ?? 1; - - const uniqueD = `${Date.now()}-${Math.random() - .toString(36) - .substring(2, 10)}`; - - // Always mark "deleted" false for new posts - const contentObject = { - version, - deleted: false, - isPrivate: videoData.isPrivate || false, - title: videoData.title, - magnet: finalMagnet, - thumbnail: videoData.thumbnail, - description: videoData.description, - mode: videoData.mode, - }; - - const event = { - kind: 30078, - pubkey, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ["t", "video"], - ["d", uniqueD], - ], - content: JSON.stringify(contentObject), - }; - - if (isDevMode) { - console.log("Event content after stringify:", event.content); - console.log("Using d tag:", uniqueD); - } - - try { - const signedEvent = await window.nostr.signEvent(event); - if (isDevMode) { - console.log("Signed event:", signedEvent); - } - - await Promise.all( - this.relays.map(async (url) => { - try { - await this.pool.publish([url], signedEvent); - if (isDevMode) { - console.log(`Event published to ${url}`); - } - } catch (err) { - if (isDevMode) { - console.error(`Failed to publish to ${url}:`, err.message); - } - } - }) - ); - - return signedEvent; - } catch (error) { - if (isDevMode) { - console.error("Failed to sign event:", error.message); - } - throw new Error("Failed to sign event."); - } - } - - /** - * Edits an existing video event by reusing the same "d" tag. - * Allows toggling isPrivate on/off and re-encrypting or decrypting the magnet. - */ - // Minimal fix: ensures we only ever encrypt once per edit operation - async editVideo(originalEvent, updatedVideoData, pubkey) { - if (!pubkey) { - throw new Error("User is not logged in."); - } - if (originalEvent.pubkey !== pubkey) { - throw new Error("You do not own this event (different pubkey)."); - } - - if (isDevMode) { - console.log("Editing video event:", originalEvent); - console.log("New video data:", updatedVideoData); - } - - // Grab the d tag from the original event - const dTag = originalEvent.tags.find((tag) => tag[0] === "d"); - if (!dTag) { - throw new Error( - 'This event has no "d" tag, cannot edit as addressable kind=30078.' - ); - } - const existingD = dTag[1]; - - // Parse old content - const oldContent = JSON.parse(originalEvent.content || "{}"); - if (isDevMode) { - console.log("Old content:", oldContent); - } - - // Keep old version & deleted status - const oldVersion = oldContent.version ?? 1; - const oldDeleted = oldContent.deleted === true; - const newVersion = updatedVideoData.version ?? oldVersion; - - const oldWasPrivate = oldContent.isPrivate === true; - - // 1) If old was private, decrypt the old magnet once => oldPlainMagnet - let oldPlainMagnet = oldContent.magnet || ""; - if (oldWasPrivate && oldPlainMagnet) { - oldPlainMagnet = fakeDecrypt(oldPlainMagnet); - } - - // 2) If updatedVideoData.isPrivate is explicitly set, use that; else keep the old isPrivate - const newIsPrivate = - typeof updatedVideoData.isPrivate === "boolean" - ? updatedVideoData.isPrivate - : oldContent.isPrivate ?? false; - - // 3) The user might type a new magnet or keep oldPlainMagnet - const userTypedMagnet = (updatedVideoData.magnet || "").trim(); - const finalPlainMagnet = userTypedMagnet || oldPlainMagnet; - - // 4) If new is private => encrypt finalPlainMagnet once; otherwise store plaintext - let finalMagnet = finalPlainMagnet; - if (newIsPrivate) { - finalMagnet = fakeEncrypt(finalPlainMagnet); - } - - // Build updated content - const contentObject = { - version: newVersion, - deleted: oldDeleted, - isPrivate: newIsPrivate, - title: updatedVideoData.title, - magnet: finalMagnet, - thumbnail: updatedVideoData.thumbnail, - description: updatedVideoData.description, - mode: updatedVideoData.mode, - }; - - if (isDevMode) { - console.log("Building updated content object:", contentObject); - } - - const event = { - kind: 30078, - pubkey, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ["t", "video"], - ["d", existingD], - ], - content: JSON.stringify(contentObject), - }; - - if (isDevMode) { - console.log("Reusing d tag:", existingD); - console.log("Updated event content:", event.content); - } - - try { - const signedEvent = await window.nostr.signEvent(event); - if (isDevMode) { - console.log("Signed edited event:", signedEvent); - } - - // Publish to all relays - await Promise.all( - this.relays.map(async (url) => { - try { - await this.pool.publish([url], signedEvent); - if (isDevMode) { - console.log( - `Edited event published to ${url} (d="${existingD}")` - ); - } - } catch (err) { - if (isDevMode) { - console.error( - `Failed to publish edited event to ${url}:`, - err.message - ); - } - } - }) - ); - - return signedEvent; - } catch (error) { - if (isDevMode) { - console.error("Failed to sign edited event:", error.message); - } - throw new Error("Failed to sign edited event."); - } - } - - /** - * Soft-delete or hide an existing video by marking content as "deleted: true" - * and republishing with same (kind=30078, pubkey, d) address. - */ - async deleteVideo(originalEvent, pubkey) { - if (!pubkey) { - throw new Error("User is not logged in."); - } - if (originalEvent.pubkey !== pubkey) { - throw new Error("You do not own this event (different pubkey)."); - } - - if (isDevMode) { - console.log("Deleting video event:", originalEvent); - } - - const dTag = originalEvent.tags.find((tag) => tag[0] === "d"); - if (!dTag) { - throw new Error( - 'This event has no "d" tag, cannot delete as addressable kind=30078.' - ); - } - const existingD = dTag[1]; - - const oldContent = JSON.parse(originalEvent.content || "{}"); - const oldVersion = oldContent.version ?? 1; - - const contentObject = { - version: oldVersion, - deleted: true, - title: oldContent.title || "", - magnet: "", - thumbnail: "", - description: "This video has been deleted.", - mode: oldContent.mode || "live", - isPrivate: oldContent.isPrivate || false, - }; - - const event = { - kind: 30078, - pubkey, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ["t", "video"], - ["d", existingD], - ], - content: JSON.stringify(contentObject), - }; - - if (isDevMode) { - console.log("Reusing d tag for delete:", existingD); - console.log("Deleted event content:", event.content); - } - - try { - const signedEvent = await window.nostr.signEvent(event); - if (isDevMode) { - console.log("Signed deleted event:", signedEvent); - } - - await Promise.all( - this.relays.map(async (url) => { - try { - await this.pool.publish([url], signedEvent); - if (isDevMode) { - console.log( - `Deleted event published to ${url} (d="${existingD}")` - ); - } - } catch (err) { - if (isDevMode) { - console.error( - `Failed to publish deleted event to ${url}:`, - err.message - ); - } - } - }) - ); - - return signedEvent; - } catch (error) { - if (isDevMode) { - console.error("Failed to sign deleted event:", error.message); - } - throw new Error("Failed to sign deleted event."); - } - } - - /** - * Fetches videos from all configured relays. - */ - async fetchVideos() { - const filter = { - kinds: [30078], - "#t": ["video"], - limit: 1000, - since: 0, - }; - - const videoEvents = new Map(); - - if (isDevMode) { - console.log("[fetchVideos] Starting fetch from all relays..."); - console.log("[fetchVideos] Filter:", filter); - } - - try { - await Promise.all( - this.relays.map(async (url) => { - if (isDevMode) console.log(`[fetchVideos] Querying relay: ${url}`); - - try { - const events = await this.pool.list([url], [filter]); - - if (isDevMode) { - console.log(`Events from ${url}:`, events.length); - if (events.length > 0) { - events.forEach((evt, idx) => { - console.log( - `[fetchVideos] [${url}] Event[${idx}] ID: ${evt.id} | pubkey: ${evt.pubkey} | created_at: ${evt.created_at}` - ); - }); - } - } - - events.forEach((event) => { - try { - const content = JSON.parse(event.content); - - // If deleted == true, it overrides older notes - if (content.deleted === true) { - videoEvents.delete(event.id); - return; - } - - // If we haven't seen this event.id before, store it - if (!videoEvents.has(event.id)) { - videoEvents.set(event.id, { - id: event.id, - version: content.version ?? 1, - isPrivate: content.isPrivate ?? false, - title: content.title || "", - magnet: content.magnet || "", - thumbnail: content.thumbnail || "", - description: content.description || "", - mode: content.mode || "live", - pubkey: event.pubkey, - created_at: event.created_at, - tags: event.tags, - }); - } - } catch (parseError) { - if (isDevMode) { - console.error( - "[fetchVideos] Event parsing error:", - parseError - ); - } - } - }); - } catch (relayError) { - if (isDevMode) { - console.error( - `[fetchVideos] Error fetching from ${url}:`, - relayError - ); - } - } - }) - ); - - const videos = Array.from(videoEvents.values()).sort( - (a, b) => b.created_at - a.created_at - ); - - // Apply access control filtering - const filteredVideos = accessControl.filterVideos(videos); - - if (isDevMode) { - console.log("[fetchVideos] All relays have responded."); - console.log( - `[fetchVideos] Total unique video events: ${videoEvents.size}` - ); - console.log( - `[fetchVideos] Videos after filtering: ${filteredVideos.length}` - ); - } - - return filteredVideos; - } catch (error) { - if (isDevMode) { - console.error("FETCH VIDEOS ERROR:", error); - } - return []; - } - } - - /** - * Validates video content structure. - */ - isValidVideo(content) { - try { - const isValid = - content && - typeof content === "object" && - typeof content.title === "string" && - content.title.length > 0 && - typeof content.magnet === "string" && - content.magnet.length > 0 && - typeof content.mode === "string" && - ["dev", "live"].includes(content.mode) && - (typeof content.thumbnail === "string" || - typeof content.thumbnail === "undefined") && - (typeof content.description === "string" || - typeof content.description === "undefined"); - - if (isDevMode && !isValid) { - console.log("Invalid video content:", content); - console.log("Validation details:", { - hasTitle: typeof content.title === "string", - hasMagnet: typeof content.magnet === "string", - hasMode: typeof content.mode === "string", - validThumbnail: - typeof content.thumbnail === "string" || - typeof content.thumbnail === "undefined", - validDescription: - typeof content.description === "string" || - typeof content.description === "undefined", - }); - } - - return isValid; - } catch (error) { - if (isDevMode) { - console.error("Error validating video:", error); - } - return false; - } - } -} - -export const nostrClient = new NostrClient(); diff --git a/refactoring/js/old/webtorrent.js b/refactoring/js/old/webtorrent.js deleted file mode 100644 index 5800a21..0000000 --- a/refactoring/js/old/webtorrent.js +++ /dev/null @@ -1,286 +0,0 @@ -// js/webtorrent.js - -import WebTorrent from 'https://esm.sh/webtorrent' - -export class TorrentClient { - constructor() { - this.client = new WebTorrent() - this.currentTorrent = null - this.TIMEOUT_DURATION = 60000 // 60 seconds - this.statsInterval = null - } - - log(msg) { - console.log(msg) - } - - async isBrave() { - return (navigator.brave?.isBrave && await navigator.brave.isBrave()) || false - } - - async waitForServiceWorkerActivation(registration) { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Service worker activation timeout')) - }, this.TIMEOUT_DURATION) - - this.log('Waiting for service worker activation...') - - const checkActivation = () => { - if (registration.active) { - clearTimeout(timeout) - this.log('Service worker is active') - resolve(registration) - return true - } - return false - } - - if (checkActivation()) return - - registration.addEventListener('activate', () => { - checkActivation() - }) - - if (registration.waiting) { - this.log('Service worker is waiting, sending skip waiting message') - registration.waiting.postMessage({ type: 'SKIP_WAITING' }) - } - - registration.addEventListener('statechange', () => { - checkActivation() - }) - }) - } - - async setupServiceWorker() { - try { - const isBraveBrowser = await this.isBrave() - - if (!window.isSecureContext) { - throw new Error('HTTPS or localhost required') - } - - if (!('serviceWorker' in navigator) || !navigator.serviceWorker) { - throw new Error('Service Worker not supported or disabled') - } - - if (isBraveBrowser) { - this.log('Checking Brave configuration...') - - if (!navigator.serviceWorker) { - throw new Error('Please enable Service Workers in Brave Shield settings') - } - - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - throw new Error('Please enable WebRTC in Brave Shield settings') - } - - const registrations = await navigator.serviceWorker.getRegistrations() - for (const registration of registrations) { - await registration.unregister() - } - await new Promise(resolve => setTimeout(resolve, 1000)) - } - - const currentPath = window.location.pathname - const basePath = currentPath.substring(0, currentPath.lastIndexOf('/') + 1) - - this.log('Registering service worker...') - const registration = await navigator.serviceWorker.register('./sw.min.js', { - scope: basePath, - updateViaCache: 'none' - }) - this.log('Service worker registered') - - if (registration.installing) { - this.log('Waiting for installation...') - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Installation timeout')) - }, this.TIMEOUT_DURATION) - - registration.installing.addEventListener('statechange', (e) => { - this.log('Service worker state:', e.target.state) - if (e.target.state === 'activated' || e.target.state === 'redundant') { - clearTimeout(timeout) - resolve() - } - }) - }) - } - - await this.waitForServiceWorkerActivation(registration) - this.log('Service worker activated') - - const readyRegistration = await Promise.race([ - navigator.serviceWorker.ready, - new Promise((_, reject) => - setTimeout(() => reject(new Error('Service worker ready timeout')), this.TIMEOUT_DURATION) - ) - ]) - - if (!readyRegistration.active) { - throw new Error('Service worker not active after ready state') - } - - this.log('Service worker ready') - return registration - } catch (error) { - this.log('Service worker setup error:', error) - throw error - } - } - - formatBytes(bytes) { - if (bytes === 0) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` - } - - async streamVideo(magnetURI, videoElement) { - try { - // Setup service worker first - const registration = await this.setupServiceWorker() - - if (!registration || !registration.active) { - throw new Error('Service worker setup failed') - } - - // Create WebTorrent server AFTER service worker is ready - this.client.createServer({ controller: registration }) - this.log('WebTorrent server created') - - return new Promise((resolve, reject) => { - this.log('Starting torrent download') - this.client.add(magnetURI, torrent => { - this.log('Torrent added: ' + torrent.name) - 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) status.textContent = `Loading ${torrent.name}...` - - const file = torrent.files.find(file => - file.name.endsWith('.mp4') || - file.name.endsWith('.webm') || - file.name.endsWith('.mkv') - ) - - if (!file) { - const error = new Error('No compatible video file found in torrent') - this.log(error.message) - if (status) status.textContent = 'Error: No video file found' - reject(error) - return - } - - videoElement.muted = true - videoElement.crossOrigin = 'anonymous' - - videoElement.addEventListener('error', (e) => { - const error = e.target.error - this.log('Video error:', error) - if (error) { - this.log('Error code:', error.code) - this.log('Error message:', error.message) - } - if (status) status.textContent = 'Error playing video. Try disabling Brave Shields.' - }) - - videoElement.addEventListener('canplay', () => { - const playPromise = videoElement.play() - if (playPromise !== undefined) { - playPromise - .then(() => this.log('Autoplay started')) - .catch(err => { - this.log('Autoplay failed:', err) - if (status) status.textContent = 'Click to play video' - videoElement.addEventListener('click', () => { - videoElement.play() - .then(() => this.log('Play started by user')) - .catch(err => this.log('Play failed:', err)) - }, { once: true }) - }) - } - }) - - videoElement.addEventListener('loadedmetadata', () => { - this.log('Video metadata loaded') - if (videoElement.duration === Infinity || isNaN(videoElement.duration)) { - this.log('Invalid duration, attempting to fix...') - videoElement.currentTime = 1e101 - videoElement.currentTime = 0 - } - }) - - try { - file.streamTo(videoElement) - this.log('Streaming started') - - // Update stats every second - this.statsInterval = setInterval(() => { - if (!document.body.contains(videoElement)) { - clearInterval(this.statsInterval) - return - } - - const percentage = torrent.progress * 100 - if (progress) progress.style.width = `${percentage}%` - if (peers) peers.textContent = `Peers: ${torrent.numPeers}` - if (speed) speed.textContent = `${this.formatBytes(torrent.downloadSpeed)}/s` - if (downloaded) downloaded.textContent = - `${this.formatBytes(torrent.downloaded)} / ${this.formatBytes(torrent.length)}` - - if (status) { - status.textContent = torrent.progress === 1 - ? `${torrent.name}` - : `Loading ${torrent.name}...` - } - }, 1000) - - this.currentTorrent = torrent - resolve() - } catch (error) { - this.log('Streaming error:', error) - if (status) status.textContent = 'Error starting video stream' - reject(error) - } - - torrent.on('error', err => { - this.log('Torrent error:', err) - if (status) status.textContent = 'Error loading video' - clearInterval(this.statsInterval) - reject(err) - }) - }) - }) - } catch (error) { - this.log('Failed to setup video streaming:', error) - throw error - } - } - - async cleanup() { - try { - if (this.statsInterval) { - clearInterval(this.statsInterval) - } - if (this.currentTorrent) { - this.currentTorrent.destroy() - } - if (this.client) { - await this.client.destroy() - this.client = new WebTorrent() // Create a new client for future use - } - } catch (error) { - this.log('Cleanup error:', error) - } - } -} - -export const torrentClient = new TorrentClient() \ No newline at end of file diff --git a/refactoring/js/services/nostr/client.js b/refactoring/js/services/nostr/client.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/services/nostr/events.js b/refactoring/js/services/nostr/events.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/services/nostr/profiles.js b/refactoring/js/services/nostr/profiles.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/services/nostr/relays.js b/refactoring/js/services/nostr/relays.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/services/torrent/client.js b/refactoring/js/services/torrent/client.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/services/torrent/stats.js b/refactoring/js/services/torrent/stats.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/services/torrent/stream.js b/refactoring/js/services/torrent/stream.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/utils/Notifications.js b/refactoring/js/utils/Notifications.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/utils/htmlUtils.js b/refactoring/js/utils/htmlUtils.js index e69de29..320a9df 100644 --- a/refactoring/js/utils/htmlUtils.js +++ b/refactoring/js/utils/htmlUtils.js @@ -0,0 +1,9 @@ +// js/utils/htmlUtils.js +export function escapeHTML(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/refactoring/js/utils/logger.js b/refactoring/js/utils/logger.js deleted file mode 100644 index e69de29..0000000 diff --git a/refactoring/js/utils/timeUtils.js b/refactoring/js/utils/timeUtils.js index e69de29..49b37e3 100644 --- a/refactoring/js/utils/timeUtils.js +++ b/refactoring/js/utils/timeUtils.js @@ -0,0 +1,21 @@ +// js/utils/timeUtils.js +export function 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`; + } + } + + return "just now"; +}