+ ${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...
+
+
+
+
+
+
+
+
+
+
+
+
+
From 169f0c952ce53237aaa964750036a5c68d31b9ad Mon Sep 17 00:00:00 2001
From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
Date: Sun, 28 Sep 2025 21:23:33 -0400
Subject: [PATCH 2/3] Prevent moderators from blacklisting themselves
---
js/accessControl.js | 5 +++++
js/adminListStore.js | 5 +++++
js/app.js | 5 +++++
3 files changed, 15 insertions(+)
diff --git a/js/accessControl.js b/js/accessControl.js
index 5dea0ae4..bb19970d 100644
--- a/js/accessControl.js
+++ b/js/accessControl.js
@@ -283,6 +283,11 @@ class AccessControl {
return { ok: false, error: "invalid npub" };
}
+ const normalizedActor = normalizeNpub(actorNpub);
+ if (normalizedActor && normalized === normalizedActor) {
+ return { ok: false, error: "self" };
+ }
+
if (this.isSuperAdmin(normalized) || this.isAdminEditor(normalized)) {
return { ok: false, error: "immutable" };
}
diff --git a/js/adminListStore.js b/js/adminListStore.js
index 706f893e..3b9dd969 100644
--- a/js/adminListStore.js
+++ b/js/adminListStore.js
@@ -448,6 +448,7 @@ async function persistNostrState(actorNpub, updates = {}) {
}
const sanitizedUpdates = {};
+ const actorNormalized = normalizeNpub(actorNpub);
if (Array.isArray(updates.editors)) {
sanitizedUpdates.editors = sanitizeNpubList(updates.editors).filter((npub) => {
@@ -470,6 +471,10 @@ async function persistNostrState(actorNpub, updates = {}) {
...((sanitizedUpdates.editors || []).map(normalizeNpub) || []),
]);
+ if (actorNormalized) {
+ editorGuard.add(actorNormalized);
+ }
+
sanitizedUpdates.blacklist = sanitizeNpubList(updates.blacklist).filter(
(npub) => {
const normalized = normalizeNpub(npub);
diff --git a/js/app.js b/js/app.js
index 33599efe..d9b25e6d 100644
--- a/js/app.js
+++ b/js/app.js
@@ -4304,6 +4304,8 @@ class bitvidApp {
return "Please provide a valid npub address.";
case "immutable":
return "That account cannot be modified.";
+ case "self":
+ return "You cannot blacklist yourself.";
case "forbidden":
return "You do not have permission to perform that action.";
case "nostr-unavailable":
@@ -6913,6 +6915,9 @@ class bitvidApp {
} else {
const code = result?.error || "unknown";
switch (code) {
+ case "self":
+ this.showError("You cannot blacklist yourself.");
+ break;
case "immutable":
this.showError(
"Moderators cannot blacklist the super admin or fellow moderators."
From bd345f99d2e5d1c57ef8302c48b9e5399a1e58bd Mon Sep 17 00:00:00 2001
From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
Date: Sun, 28 Sep 2025 21:28:04 -0400
Subject: [PATCH 3/3] Fix channel overflow menu layout
---
views/channel-profile.html | 36 ++++++++++++++++--------------------
1 file changed, 16 insertions(+), 20 deletions(-)
diff --git a/views/channel-profile.html b/views/channel-profile.html
index 05043d92..5a9a1ab4 100644
--- a/views/channel-profile.html
+++ b/views/channel-profile.html
@@ -1,16 +1,14 @@