// 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 = `${new Date(video.created_at * 1000).toLocaleString()}