mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add moderator blacklist controls to menus
This commit is contained in:
1 parent
a0356967ce
commit
d39ed45c5a
4 files changed
+397
-12
No files matched your search
@@ -94,6 +94,14 @@
|
||||
>
|
||||
Copy link
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
|
||||
data-action="blacklist-author"
|
||||
data-context="modal"
|
||||
data-blacklist-menu="true"
|
||||
>
|
||||
Blacklist creator
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
|
||||
data-action="block-author"
|
||||
|
||||
@@ -4054,6 +4054,20 @@ class bitvidApp {
|
||||
return this.currentUserNpub;
|
||||
}
|
||||
|
||||
canCurrentUserManageBlacklist() {
|
||||
const actorNpub = this.getCurrentUserNpub();
|
||||
if (!actorNpub) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return accessControl.canEditAdminLists(actorNpub);
|
||||
} catch (error) {
|
||||
console.warn("Unable to verify blacklist permissions:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ensureAdminActor(requireSuperAdmin = false) {
|
||||
const actorNpub = this.getCurrentUserNpub();
|
||||
if (!actorNpub) {
|
||||
@@ -5992,6 +6006,7 @@ class bitvidApp {
|
||||
this.getShareUrlBase() ||
|
||||
`${window.location?.origin || ""}${window.location?.pathname || ""}` ||
|
||||
(window.location?.href ? window.location.href.split(/[?#]/)[0] : "");
|
||||
const canManageBlacklist = this.canCurrentUserManageBlacklist();
|
||||
|
||||
// 3) Build each card
|
||||
dedupedVideos.forEach((video, index) => {
|
||||
@@ -6073,6 +6088,13 @@ class bitvidApp {
|
||||
`
|
||||
: "";
|
||||
|
||||
const blacklistMenuItem = canManageBlacklist
|
||||
? `
|
||||
<button class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white" data-action="blacklist-author" data-author="${video.pubkey || ""}">
|
||||
Blacklist creator
|
||||
</button>`
|
||||
: "";
|
||||
|
||||
const moreMenu = `
|
||||
<div class="relative inline-block ml-1 overflow-visible" data-more-menu-wrapper="true">
|
||||
<button
|
||||
@@ -6098,6 +6120,7 @@ class bitvidApp {
|
||||
<button class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700" data-action="copy-link" data-event-id="${video.id || ""}">
|
||||
Copy link
|
||||
</button>
|
||||
${blacklistMenuItem}
|
||||
<button class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white" data-action="block-author" data-author="${video.pubkey || ""}">
|
||||
Block creator
|
||||
</button>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
+83
-11
@@ -12,18 +12,90 @@
|
||||
/>
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
|
||||
<div class="absolute bottom-4 left-4 flex items-center">
|
||||
<div class="w-16 h-16 rounded-full overflow-hidden border-2 border-white">
|
||||
<img
|
||||
id="channelAvatar"
|
||||
src="assets/svg/default-profile.svg"
|
||||
alt="Avatar"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div
|
||||
class="absolute bottom-4 left-4 right-4 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="w-16 h-16 rounded-full overflow-hidden border-2 border-white">
|
||||
<img
|
||||
id="channelAvatar"
|
||||
src="assets/svg/default-profile.svg"
|
||||
alt="Avatar"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4 text-white">
|
||||
<h2 id="channelName" class="text-xl font-bold">User Name</h2>
|
||||
<p id="channelNpub" class="text-sm opacity-80">npub...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 text-white">
|
||||
<h2 id="channelName" class="text-xl font-bold">User Name</h2>
|
||||
<p id="channelNpub" class="text-sm opacity-80">npub...</p>
|
||||
<div class="flex items-center gap-2 self-start sm:self-end">
|
||||
<button
|
||||
id="channelShareBtn"
|
||||
type="button"
|
||||
class="icon-button"
|
||||
aria-label="Share channel"
|
||||
>
|
||||
<img
|
||||
src="assets/svg/share-video.svg"
|
||||
alt="Share channel"
|
||||
class="icon-image"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="relative inline-block overflow-visible"
|
||||
data-more-menu-wrapper="true"
|
||||
>
|
||||
<button
|
||||
id="channelMoreBtn"
|
||||
type="button"
|
||||
class="icon-button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-label="Channel options"
|
||||
data-more-dropdown="channel-profile"
|
||||
>
|
||||
<img src="assets/svg/ellipsis.svg" alt="More" class="w-5 h-5" />
|
||||
</button>
|
||||
<div
|
||||
id="moreDropdown-channel-profile"
|
||||
class="hidden absolute right-0 top-12 w-48 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
|
||||
role="menu"
|
||||
data-more-menu="true"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
|
||||
data-action="copy-npub"
|
||||
data-context="channel-profile"
|
||||
>
|
||||
Copy npub
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
|
||||
data-action="blacklist-author"
|
||||
data-context="channel-profile"
|
||||
data-blacklist-menu="true"
|
||||
>
|
||||
Blacklist channel
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
|
||||
data-action="block-author"
|
||||
data-context="channel-profile"
|
||||
>
|
||||
Block channel
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
|
||||
data-action="report"
|
||||
data-context="channel-profile"
|
||||
>
|
||||
Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user