+ ${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 @@
/>
-
-
-

+
+
+
+

+
+
-
-
User Name
-
npub...
+
+
+
+
+
+
+
+
+
+
+
+
+