// js/channelProfile.js import { nostrClient } from "./nostr.js"; import { app } from "./app.js"; import { subscriptions } from "./subscriptions.js"; // <-- NEW import import { initialBlacklist, initialWhitelist } from "./lists.js"; import { isWhitelistEnabled } from "./config.js"; /** * Initialize the channel profile view. * Called when #view=channel-profile&npub=... */ export async function initChannelProfileView() { // 1) Get npub from hash const hashParams = new URLSearchParams(window.location.hash.slice(1)); const npub = hashParams.get("npub"); if (!npub) { console.error( "No npub found in hash (e.g. #view=channel-profile&npub=...)" ); return; } // 2) Decode npub => hex pubkey let hexPub; try { const decoded = window.NostrTools.nip19.decode(npub); if (decoded.type === "npub" && decoded.data) { hexPub = decoded.data; } else { throw new Error("Invalid npub decoding result."); } } catch (err) { console.error("Error decoding npub:", err); return; } // 3) If user is logged in, load subscriptions and show sub/unsub button if (app.pubkey) { await subscriptions.loadSubscriptions(app.pubkey); renderSubscribeButton(hexPub); } else { const btn = document.getElementById("subscribeBtnArea"); if (btn) btn.classList.add("hidden"); } // 4) Load user’s profile (banner, avatar, etc.) await loadUserProfile(hexPub); // 5) Load user’s videos (filtered + rendered like the home feed) await loadUserVideos(hexPub); } /** * Renders a Subscribe / Unsubscribe button with an icon, * using color #fe0032 and the subscribe-button-icon.svg on the left. */ function renderSubscribeButton(channelHex) { const container = document.getElementById("subscribeBtnArea"); if (!container) return; container.classList.remove("hidden"); const alreadySubscribed = subscriptions.isSubscribed(channelHex); // We'll use #fe0032 for both subscribe/unsubscribe, // and the same icon. If you prefer separate logic for unsub, you can do it here. container.innerHTML = ` `; const toggleBtn = document.getElementById("subscribeToggleBtn"); if (toggleBtn) { toggleBtn.addEventListener("click", async () => { if (!app.pubkey) { console.error("Not logged in => cannot subscribe/unsubscribe."); return; } try { if (alreadySubscribed) { await subscriptions.removeChannel(channelHex, app.pubkey); } else { await subscriptions.addChannel(channelHex, app.pubkey); } // Re-render the button so it toggles state renderSubscribeButton(channelHex); } catch (err) { console.error("Failed to update subscription:", err); } }); } } /** * Fetches and displays the user's metadata (kind=0). */ async function loadUserProfile(pubkey) { try { const events = await nostrClient.pool.list(nostrClient.relays, [ { kinds: [0], authors: [pubkey], limit: 1 }, ]); if (events.length && events[0].content) { const meta = JSON.parse(events[0].content); // Banner const bannerEl = document.getElementById("channelBanner"); if (bannerEl) { bannerEl.src = meta.banner || "assets/jpg/default-banner.jpg"; } // Avatar const avatarEl = document.getElementById("channelAvatar"); if (avatarEl) { avatarEl.src = meta.picture || "assets/svg/default-profile.svg"; } // Channel Name const nameEl = document.getElementById("channelName"); if (nameEl) { nameEl.textContent = meta.display_name || meta.name || "Unknown User"; } // Channel npub const channelNpubEl = document.getElementById("channelNpub"); if (channelNpubEl) { const userNpub = window.NostrTools.nip19.npubEncode(pubkey); channelNpubEl.textContent = userNpub; } // About/Description const aboutEl = document.getElementById("channelAbout"); if (aboutEl) { aboutEl.textContent = meta.about || ""; } // Website const websiteEl = document.getElementById("channelWebsite"); if (websiteEl) { if (meta.website) { websiteEl.href = meta.website; websiteEl.textContent = meta.website; } else { websiteEl.textContent = ""; websiteEl.removeAttribute("href"); } } // Lightning Address const lnEl = document.getElementById("channelLightning"); if (lnEl) { lnEl.textContent = meta.lud16 || meta.lud06 || "No lightning address found."; } } else { console.warn("No metadata found for this user."); } } catch (err) { console.error("Failed to fetch user profile data:", err); } } /** * Fetches and displays this user's videos (kind=30078). * Filters out older overshadowed notes, blacklisted, etc. */ async function loadUserVideos(pubkey) { try { // 1) Build filter for videos from this pubkey const filter = { kinds: [30078], authors: [pubkey], "#t": ["video"], limit: 200, }; // 2) Collect raw events from all relays const events = []; for (const url of nostrClient.relays) { try { const result = await nostrClient.pool.list([url], [filter]); events.push(...result); } catch (relayErr) { console.error(`Relay error (${url}):`, relayErr); } } // 3) Convert to "video" objects let videos = []; for (const evt of events) { const vid = localConvertEventToVideo(evt); if (!vid.invalid && !vid.deleted) { videos.push(vid); } } // 4) Deduplicate older overshadowed versions => newest only videos = dedupeToNewestByRoot(videos); // 5) Filter out blacklisted IDs / authors videos = videos.filter((video) => { // Event-level blacklisting if (app.blacklistedEventIds.has(video.id)) return false; // Author-level const authorNpub = app.safeEncodeNpub(video.pubkey) || video.pubkey; if (initialBlacklist.includes(authorNpub)) return false; if (isWhitelistEnabled && !initialWhitelist.includes(authorNpub)) { return false; } return true; }); // 6) Sort newest first videos.sort((a, b) => b.created_at - a.created_at); // 7) Render them const container = document.getElementById("channelVideoList"); if (!container) { console.warn("channelVideoList element not found in DOM."); return; } container.innerHTML = ""; if (!videos.length) { container.innerHTML = `

No videos to display.

`; return; } const fragment = document.createDocumentFragment(); const allKnownEventsArray = Array.from(nostrClient.allEvents.values()); videos.forEach((video, index) => { // Decrypt if user owns a private video if ( video.isPrivate && video.pubkey === nostrClient.pubkey && !video.alreadyDecrypted ) { video.magnet = fakeDecrypt(video.magnet); video.alreadyDecrypted = true; } // Check if user can edit const canEdit = video.pubkey === app.pubkey; let hasOlder = false; if (canEdit && video.videoRootId) { hasOlder = app.hasOlderVersion(video, allKnownEventsArray); } const revertButton = hasOlder ? ` ` : ""; let gearMenu = ""; if (canEdit) { gearMenu = `
`; } // Fallback thumbnail const fallbackThumb = "assets/jpg/video-thumbnail-fallback.jpg"; const safeThumb = video.thumbnail || fallbackThumb; const safeTitle = escapeHTML(video.title); const cardEl = document.createElement("div"); cardEl.classList.add( "bg-gray-900", "rounded-lg", "overflow-hidden", "shadow-lg", "hover:shadow-2xl", "transition-all", "duration-300" ); cardEl.innerHTML = `
${safeTitle}

${safeTitle}

${new Date(video.created_at * 1000).toLocaleString()}

${gearMenu}
`; // Clicking the card => open the video modal cardEl.addEventListener("click", () => { app.playVideoByEventId(video.id); }); fragment.appendChild(cardEl); }); container.appendChild(fragment); // Lazy-load images const lazyEls = container.querySelectorAll("[data-lazy]"); lazyEls.forEach((el) => app.mediaLoader.observe(el)); // Gear menu toggles const gearButtons = container.querySelectorAll("[data-settings-dropdown]"); gearButtons.forEach((btn) => { btn.addEventListener("click", (ev) => { ev.stopPropagation(); const idx = btn.getAttribute("data-settings-dropdown"); const dropdown = document.getElementById(`settingsDropdown-${idx}`); if (dropdown) { dropdown.classList.toggle("hidden"); } }); }); // Edit handler const editBtns = container.querySelectorAll("[data-edit-index]"); editBtns.forEach((btn) => { btn.addEventListener("click", (ev) => { ev.stopPropagation(); const idx = parseInt(btn.getAttribute("data-edit-index"), 10); const dropdown = document.getElementById(`settingsDropdown-${idx}`); if (dropdown) dropdown.classList.add("hidden"); app.handleEditVideo(idx); }); }); // Revert handler const revertBtns = container.querySelectorAll("[data-revert-index]"); revertBtns.forEach((btn) => { btn.addEventListener("click", (ev) => { ev.stopPropagation(); const idx = parseInt(btn.getAttribute("data-revert-index"), 10); const dropdown = document.getElementById(`settingsDropdown-${idx}`); if (dropdown) dropdown.classList.add("hidden"); app.handleRevertVideo(idx); }); }); // Delete All handler const deleteAllBtns = container.querySelectorAll("[data-delete-all-index]"); deleteAllBtns.forEach((btn) => { btn.addEventListener("click", (ev) => { ev.stopPropagation(); const idx = parseInt(btn.getAttribute("data-delete-all-index"), 10); const dropdown = document.getElementById(`settingsDropdown-${idx}`); if (dropdown) dropdown.classList.add("hidden"); app.handleFullDeleteVideo(idx); }); }); } catch (err) { console.error("Error loading user videos:", err); } } /** * Minimal placeholder for private video decryption. */ function fakeDecrypt(str) { return str.split("").reverse().join(""); } /** * Keep only the newest version of each video root. */ function dedupeToNewestByRoot(videos) { const map = new Map(); for (const vid of videos) { const rootId = vid.videoRootId || vid.id; const existing = map.get(rootId); if (!existing || vid.created_at > existing.created_at) { map.set(rootId, vid); } } return Array.from(map.values()); } /** * Convert raw event => "video" object. */ function localConvertEventToVideo(event) { try { const content = JSON.parse(event.content || "{}"); const isSupportedVersion = content.version >= 2; const hasRequiredFields = !!(content.title && content.magnet); if (!isSupportedVersion) { return { id: event.id, invalid: true, reason: "version <2" }; } if (!hasRequiredFields) { return { id: event.id, invalid: true, reason: "missing title/magnet" }; } return { id: event.id, videoRootId: content.videoRootId || event.id, version: content.version, isPrivate: content.isPrivate ?? false, title: content.title ?? "", magnet: content.magnet ?? "", thumbnail: content.thumbnail ?? "", description: content.description ?? "", mode: content.mode ?? "live", deleted: content.deleted === true, pubkey: event.pubkey, created_at: event.created_at, tags: event.tags, invalid: false, }; } catch (err) { return { id: event.id, invalid: true, reason: "json parse error" }; } } /** * Basic escaping to avoid XSS. */ function escapeHTML(unsafe = "") { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }