diff --git a/components/video-modal.html b/components/video-modal.html index 7e3e165c..057980c9 100644 --- a/components/video-modal.html +++ b/components/video-modal.html @@ -94,6 +94,14 @@ > Copy link + ` + : ""; + const moreMenu = `
+ ${blacklistMenuItem} @@ -6748,6 +6771,20 @@ class bitvidApp { } const action = button.dataset.action || ""; + if (action === "blacklist-author") { + const canShow = this.canCurrentUserManageBlacklist(); + if (canShow && this.currentVideo && this.currentVideo.pubkey) { + button.dataset.author = this.currentVideo.pubkey; + button.classList.remove("hidden"); + button.setAttribute("aria-hidden", "false"); + } else { + delete button.dataset.author; + button.classList.add("hidden"); + button.setAttribute("aria-hidden", "true"); + } + return; + } + if (action === "open-channel" || action === "block-author") { if (this.currentVideo && this.currentVideo.pubkey) { button.dataset.author = this.currentVideo.pubkey; @@ -6766,7 +6803,7 @@ class bitvidApp { }); } - handleMoreMenuAction(action, dataset = {}) { + async handleMoreMenuAction(action, dataset = {}) { const normalized = typeof action === "string" ? action.trim() : ""; const context = dataset.context || ""; @@ -6805,6 +6842,101 @@ class bitvidApp { .catch(() => this.showError("Failed to copy the link.")); break; } + case "copy-npub": { + const explicitNpub = + typeof dataset.npub === "string" && dataset.npub.trim() + ? dataset.npub.trim() + : ""; + const authorCandidate = dataset.author || ""; + const fallbackNpub = this.safeEncodeNpub(authorCandidate); + const valueToCopy = explicitNpub || fallbackNpub; + + if (!valueToCopy) { + this.showError("No npub available to copy."); + break; + } + + try { + await navigator.clipboard.writeText(valueToCopy); + this.showSuccess("Channel npub copied to clipboard!"); + } catch (error) { + console.error("Failed to copy npub:", error); + this.showError("Failed to copy the npub."); + } + break; + } + case "blacklist-author": { + const actorNpub = this.getCurrentUserNpub(); + if (!actorNpub) { + this.showError("Please login as a moderator to manage the blacklist."); + break; + } + + try { + await accessControl.ensureReady(); + } catch (error) { + console.warn("Failed to refresh moderation state before blacklisting:", error); + } + + if (!accessControl.canEditAdminLists(actorNpub)) { + this.showError("Only moderators can manage the blacklist."); + break; + } + + let author = dataset.author || ""; + if (!author && context === "modal" && this.currentVideo?.pubkey) { + author = this.currentVideo.pubkey; + } + if (!author && this.currentVideo?.pubkey) { + author = this.currentVideo.pubkey; + } + + const explicitNpub = + typeof dataset.npub === "string" && dataset.npub.trim() + ? dataset.npub.trim() + : ""; + const targetNpub = explicitNpub || this.safeEncodeNpub(author); + + if (!targetNpub) { + this.showError("Unable to determine the creator npub."); + break; + } + + try { + const result = await accessControl.addToBlacklist( + actorNpub, + targetNpub + ); + + if (result?.ok) { + this.showSuccess("Creator added to the blacklist."); + } else { + const code = result?.error || "unknown"; + switch (code) { + case "immutable": + this.showError( + "Moderators cannot blacklist the super admin or fellow moderators." + ); + break; + case "invalid npub": + this.showError("Unable to blacklist this creator."); + break; + case "forbidden": + this.showError("Only moderators can manage the blacklist."); + break; + default: + this.showError( + "Failed to update the blacklist. Please try again." + ); + break; + } + } + } catch (error) { + console.error("Failed to add creator to blacklist:", error); + this.showError("Failed to update the blacklist. Please try again."); + } + break; + } case "block-author": { this.showSuccess("Block management coming soon."); break; diff --git a/js/channelProfile.js b/js/channelProfile.js index e448f3dd..3a3865d8 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -14,6 +14,8 @@ let currentChannelHex = null; let currentChannelNpub = null; let cachedZapButton = null; +let cachedChannelShareButton = null; +let cachedChannelMenu = null; function getChannelZapButton() { if (cachedZapButton && !document.body.contains(cachedZapButton)) { @@ -42,6 +44,170 @@ function setChannelZapVisibility(visible) { } } +function getChannelShareButton() { + if (cachedChannelShareButton && !document.body.contains(cachedChannelShareButton)) { + cachedChannelShareButton = null; + } + if (!cachedChannelShareButton) { + cachedChannelShareButton = document.getElementById("channelShareBtn"); + } + return cachedChannelShareButton; +} + +function getChannelMenuElement() { + if (cachedChannelMenu && !document.body.contains(cachedChannelMenu)) { + cachedChannelMenu = null; + } + if (!cachedChannelMenu) { + cachedChannelMenu = document.getElementById("moreDropdown-channel-profile"); + } + return cachedChannelMenu; +} + +function buildChannelShareUrl() { + if (!currentChannelNpub) { + return ""; + } + + const base = typeof app.getShareUrlBase === "function" ? app.getShareUrlBase() : ""; + if (base) { + return `${base}#view=channel-profile&npub=${encodeURIComponent(currentChannelNpub)}`; + } + + try { + const url = new URL(window.location.href); + url.search = ""; + url.hash = `#view=channel-profile&npub=${currentChannelNpub}`; + return url.toString(); + } catch (error) { + console.warn("Falling back to basic channel share URL:", error); + const origin = window.location?.origin || ""; + const pathname = window.location?.pathname || ""; + if (!origin && !pathname) { + return ""; + } + return `${origin}${pathname}#view=channel-profile&npub=${currentChannelNpub}`; + } +} + +function syncChannelShareButtonState() { + const shareBtn = getChannelShareButton(); + if (!shareBtn) { + return; + } + const hasNpub = typeof currentChannelNpub === "string" && currentChannelNpub; + shareBtn.disabled = !hasNpub; + shareBtn.setAttribute("aria-disabled", (!hasNpub).toString()); + shareBtn.classList.toggle("opacity-50", !hasNpub); + shareBtn.classList.toggle("cursor-not-allowed", !hasNpub); +} + +function setupChannelShareButton() { + const shareBtn = getChannelShareButton(); + if (!shareBtn) { + return; + } + + syncChannelShareButtonState(); + + if (shareBtn.dataset.initialized === "true") { + return; + } + + shareBtn.addEventListener("click", () => { + const shareUrl = buildChannelShareUrl(); + if (!shareUrl) { + app.showError("Could not generate channel link."); + return; + } + + navigator.clipboard + .writeText(shareUrl) + .then(() => app.showSuccess("Channel link copied to clipboard!")) + .catch(() => app.showError("Failed to copy the link.")); + }); + + shareBtn.dataset.initialized = "true"; +} + +function setupChannelMoreMenu() { + const container = document.querySelector(".channel-profile-container"); + if (!container) { + return; + } + app.attachMoreMenuHandlers(container); +} + +async function updateChannelMenuState() { + const menu = getChannelMenuElement(); + if (!menu) { + return; + } + + try { + await accessControl.ensureReady(); + } catch (error) { + console.warn("Failed to refresh moderation lists for channel menu:", error); + } + + const canBlacklist = + typeof app.canCurrentUserManageBlacklist === "function" + ? app.canCurrentUserManageBlacklist() + : false; + + const buttons = menu.querySelectorAll("button[data-action]"); + buttons.forEach((button) => { + if (!(button instanceof HTMLElement)) { + return; + } + + const action = button.dataset.action || ""; + if (action === "copy-npub") { + if (currentChannelNpub) { + button.dataset.npub = currentChannelNpub; + button.classList.remove("hidden"); + button.setAttribute("aria-hidden", "false"); + } else { + delete button.dataset.npub; + button.classList.add("hidden"); + button.setAttribute("aria-hidden", "true"); + } + return; + } + + if (action === "block-author") { + if (currentChannelHex) { + button.dataset.author = currentChannelHex; + button.classList.remove("hidden"); + button.setAttribute("aria-hidden", "false"); + } else { + delete button.dataset.author; + button.classList.add("hidden"); + button.setAttribute("aria-hidden", "true"); + } + return; + } + + if (action === "blacklist-author") { + if (canBlacklist && currentChannelHex) { + button.dataset.author = currentChannelHex; + if (currentChannelNpub) { + button.dataset.npub = currentChannelNpub; + } else { + delete button.dataset.npub; + } + button.classList.remove("hidden"); + button.setAttribute("aria-hidden", "false"); + } else { + delete button.dataset.author; + delete button.dataset.npub; + button.classList.add("hidden"); + button.setAttribute("aria-hidden", "true"); + } + } + }); +} + /** * Initialize the channel profile view. * Called when #view=channel-profile&npub=... @@ -77,6 +243,10 @@ export async function initChannelProfileView() { currentChannelHex = hexPub; currentChannelNpub = npub; + setupChannelShareButton(); + setupChannelMoreMenu(); + await updateChannelMenuState(); + // 3) If user is logged in, load subscriptions and show sub/unsub button if (app.pubkey) { await subscriptions.loadSubscriptions(app.pubkey); @@ -87,9 +257,12 @@ export async function initChannelProfileView() { } setupZapButton(); + syncChannelShareButtonState(); // 4) Load user’s profile (banner, avatar, etc.) await loadUserProfile(hexPub); + await updateChannelMenuState(); + syncChannelShareButtonState(); // 5) Load user’s videos (filtered + rendered like the home feed) await loadUserVideos(hexPub); diff --git a/views/channel-profile.html b/views/channel-profile.html index 845e08e1..05043d92 100644 --- a/views/channel-profile.html +++ b/views/channel-profile.html @@ -12,18 +12,90 @@ />
-
-
- Avatar +
+
+
+ Avatar +
+
+

User Name

+

npub...

+
-
-

User Name

-

npub...

+
+ +
+ + +