mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
816 lines
24 KiB
JavaScript
816 lines
24 KiB
JavaScript
import { normalizeDesignSystemContext } from "../designSystem.js";
|
|
import { userLogger } from "../utils/logger.js";
|
|
import {
|
|
createVideoMoreMenuPanel,
|
|
} from "./components/videoMenuRenderers.js";
|
|
import createPopover from "./overlay/popoverEngine.js";
|
|
|
|
export default class MoreMenuController {
|
|
constructor(options = {}) {
|
|
const {
|
|
document: doc = typeof document !== "undefined" ? document : null,
|
|
accessControl = null,
|
|
userBlocks = null,
|
|
subscriptions = null,
|
|
clipboard = typeof navigator !== "undefined" ? navigator.clipboard : null,
|
|
isDevMode = false,
|
|
callbacks = {},
|
|
designSystem = null,
|
|
} = options;
|
|
|
|
this.document = doc;
|
|
this.accessControl = accessControl;
|
|
this.userBlocks = userBlocks;
|
|
this.subscriptions = subscriptions;
|
|
this.clipboard = clipboard;
|
|
this.isDevMode = Boolean(isDevMode);
|
|
this.designSystem = normalizeDesignSystemContext(designSystem);
|
|
|
|
this.callbacks = {
|
|
getCurrentVideo: callbacks.getCurrentVideo || (() => null),
|
|
getCurrentUserNpub: callbacks.getCurrentUserNpub || (() => null),
|
|
getCurrentUserPubkey: callbacks.getCurrentUserPubkey || (() => null),
|
|
canCurrentUserManageBlacklist:
|
|
callbacks.canCurrentUserManageBlacklist || (() => false),
|
|
openCreatorChannel: callbacks.openCreatorChannel || (() => {}),
|
|
goToProfile: callbacks.goToProfile || (() => {}),
|
|
showError: callbacks.showError || (() => {}),
|
|
showSuccess: callbacks.showSuccess || (() => {}),
|
|
safeEncodeNpub: callbacks.safeEncodeNpub || (() => ""),
|
|
safeDecodeNpub: callbacks.safeDecodeNpub || (() => ""),
|
|
buildShareUrlFromEventId:
|
|
callbacks.buildShareUrlFromEventId || (() => ""),
|
|
handleRemoveHistoryAction:
|
|
callbacks.handleRemoveHistoryAction || (() => Promise.resolve()),
|
|
handleRepostAction:
|
|
callbacks.handleRepostAction || (() => Promise.resolve()),
|
|
handleMirrorAction:
|
|
callbacks.handleMirrorAction || (() => Promise.resolve()),
|
|
handleEnsurePresenceAction:
|
|
callbacks.handleEnsurePresenceAction || (() => Promise.resolve()),
|
|
loadVideos: callbacks.loadVideos || (() => Promise.resolve()),
|
|
onUserBlocksUpdated: callbacks.onUserBlocksUpdated || (() => {}),
|
|
};
|
|
|
|
this.moreMenuGlobalHandlerBound = false;
|
|
|
|
this.videoListView = null;
|
|
this.videoModal = null;
|
|
|
|
this.boundVideoListShareListener = null;
|
|
this.boundVideoListContextListener = null;
|
|
this.boundVideoListBlacklistHandler = null;
|
|
this.popoversByTrigger = new Map();
|
|
this.activePopover = null;
|
|
}
|
|
|
|
setVideoModal(videoModal) {
|
|
this.videoModal = videoModal || null;
|
|
}
|
|
|
|
attachVideoListView(view) {
|
|
if (view === this.videoListView) {
|
|
return;
|
|
}
|
|
|
|
this.detachVideoListView();
|
|
|
|
if (!view) {
|
|
return;
|
|
}
|
|
|
|
this.videoListView = view;
|
|
|
|
this.boundVideoListBlacklistHandler = ({ video, dataset }) => {
|
|
const detail = {
|
|
...(dataset || {}),
|
|
author: dataset?.author || video?.pubkey || "",
|
|
};
|
|
this.handleMoreMenuAction("blacklist-author", detail);
|
|
};
|
|
|
|
if (typeof view.setBlacklistHandler === "function") {
|
|
view.setBlacklistHandler(this.boundVideoListBlacklistHandler);
|
|
}
|
|
|
|
this.boundVideoListShareListener = (event) => {
|
|
const detail = event?.detail || {};
|
|
const dataset = {
|
|
...(detail.dataset || {}),
|
|
eventId:
|
|
detail.eventId ||
|
|
detail.dataset?.eventId ||
|
|
detail.video?.id ||
|
|
"",
|
|
context: detail.dataset?.context || "card",
|
|
};
|
|
const action = detail.action || "copy-link";
|
|
this.handleMoreMenuAction(action, dataset);
|
|
};
|
|
|
|
if (typeof view.addEventListener === "function") {
|
|
view.addEventListener("video:share", this.boundVideoListShareListener);
|
|
}
|
|
|
|
this.boundVideoListContextListener = (event) => {
|
|
const detail = event?.detail || {};
|
|
const dataset = {
|
|
...(detail.dataset || {}),
|
|
eventId: detail.dataset?.eventId || detail.video?.id || "",
|
|
};
|
|
this.handleMoreMenuAction(detail.action, dataset);
|
|
};
|
|
|
|
if (typeof view.addEventListener === "function") {
|
|
view.addEventListener(
|
|
"video:context-action",
|
|
this.boundVideoListContextListener,
|
|
);
|
|
}
|
|
}
|
|
|
|
detachVideoListView() {
|
|
if (!this.videoListView) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this.boundVideoListShareListener &&
|
|
typeof this.videoListView.removeEventListener === "function"
|
|
) {
|
|
this.videoListView.removeEventListener(
|
|
"video:share",
|
|
this.boundVideoListShareListener,
|
|
);
|
|
}
|
|
|
|
if (
|
|
this.boundVideoListContextListener &&
|
|
typeof this.videoListView.removeEventListener === "function"
|
|
) {
|
|
this.videoListView.removeEventListener(
|
|
"video:context-action",
|
|
this.boundVideoListContextListener,
|
|
);
|
|
}
|
|
|
|
if (typeof this.videoListView.setBlacklistHandler === "function") {
|
|
this.videoListView.setBlacklistHandler(null);
|
|
}
|
|
|
|
this.boundVideoListShareListener = null;
|
|
this.boundVideoListContextListener = null;
|
|
this.boundVideoListBlacklistHandler = null;
|
|
this.videoListView = null;
|
|
}
|
|
|
|
destroy() {
|
|
this.detachVideoListView();
|
|
this.setVideoModal(null);
|
|
|
|
this.closeAllMoreMenus({ skipView: true });
|
|
|
|
this.popoversByTrigger.forEach((entry) => {
|
|
try {
|
|
entry?.popover?.destroy?.();
|
|
} catch (error) {
|
|
if (this.isDevMode) {
|
|
userLogger.warn("[MoreMenu] Failed to destroy popover:", error);
|
|
}
|
|
}
|
|
});
|
|
this.popoversByTrigger.clear();
|
|
this.activePopover = null;
|
|
this.moreMenuGlobalHandlerBound = false;
|
|
}
|
|
|
|
ensureGlobalMoreMenuHandlers() {
|
|
this.moreMenuGlobalHandlerBound = true;
|
|
}
|
|
|
|
getHTMLElementConstructor(documentRef) {
|
|
return (
|
|
documentRef?.defaultView?.HTMLElement ||
|
|
(typeof HTMLElement !== "undefined" ? HTMLElement : null)
|
|
);
|
|
}
|
|
|
|
createPopoverRender(entry) {
|
|
return ({ document: documentRef, close }) => {
|
|
const panel = createVideoMoreMenuPanel({
|
|
document: documentRef,
|
|
video: entry.context.video,
|
|
pointerInfo: entry.context.pointerInfo,
|
|
playbackUrl: entry.context.playbackUrl,
|
|
playbackMagnet: entry.context.playbackMagnet,
|
|
canManageBlacklist: entry.context.canManageBlacklist,
|
|
context: entry.context.context || "card",
|
|
designSystem: entry.context.designSystem || this.designSystem,
|
|
});
|
|
|
|
if (!panel) {
|
|
return null;
|
|
}
|
|
|
|
const buttons = panel.querySelectorAll("button[data-action]");
|
|
buttons.forEach((button) => {
|
|
const HTMLElementCtor = this.getHTMLElementConstructor(button?.ownerDocument);
|
|
if (HTMLElementCtor && !(button instanceof HTMLElementCtor)) {
|
|
return;
|
|
}
|
|
|
|
button.addEventListener("click", async (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const dataset = { ...button.dataset };
|
|
const action = dataset.action || "";
|
|
|
|
try {
|
|
await this.handleMoreMenuAction(action, dataset);
|
|
} catch (error) {
|
|
userLogger.error("[MoreMenu] Failed to handle action:", error);
|
|
}
|
|
|
|
if (typeof entry.context.onAction === "function") {
|
|
try {
|
|
entry.context.onAction({ action, dataset, event });
|
|
} catch (callbackError) {
|
|
if (this.isDevMode) {
|
|
userLogger.warn(
|
|
"[MoreMenu] onAction callback failed",
|
|
callbackError,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof close === "function") {
|
|
close();
|
|
}
|
|
});
|
|
});
|
|
|
|
return panel;
|
|
};
|
|
}
|
|
|
|
decoratePopoverLifecycle(popover, trigger, entry) {
|
|
if (!popover) {
|
|
return null;
|
|
}
|
|
|
|
const originalOpen = popover.open?.bind(popover);
|
|
const originalClose = popover.close?.bind(popover);
|
|
const originalDestroy = popover.destroy?.bind(popover);
|
|
|
|
if (typeof originalOpen === "function") {
|
|
popover.open = async (...args) => {
|
|
const result = await originalOpen(...args);
|
|
if (result) {
|
|
this.activePopover = popover;
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
if (typeof originalClose === "function") {
|
|
popover.close = (options = {}) => {
|
|
const restoreFocus =
|
|
options?.restoreFocus !== undefined
|
|
? options.restoreFocus
|
|
: entry?.context?.restoreFocusOnClose !== false;
|
|
const result = originalClose({ restoreFocus });
|
|
if (result && this.activePopover === popover) {
|
|
this.activePopover = null;
|
|
}
|
|
if (result && typeof entry?.context?.onClose === "function") {
|
|
try {
|
|
entry.context.onClose({
|
|
trigger,
|
|
reason: options?.reason || "close",
|
|
restoreFocus,
|
|
});
|
|
} catch (error) {
|
|
if (this.isDevMode) {
|
|
userLogger.warn("[MoreMenu] onClose callback failed", error);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
if (typeof originalDestroy === "function") {
|
|
popover.destroy = (...args) => {
|
|
if (this.activePopover === popover) {
|
|
this.activePopover = null;
|
|
}
|
|
originalDestroy(...args);
|
|
if (trigger && this.popoversByTrigger.get(trigger) === entry) {
|
|
this.popoversByTrigger.delete(trigger);
|
|
}
|
|
};
|
|
}
|
|
|
|
return popover;
|
|
}
|
|
|
|
ensurePopoverForTrigger({ trigger }) {
|
|
if (!trigger) {
|
|
return null;
|
|
}
|
|
|
|
if (this.popoversByTrigger.has(trigger)) {
|
|
return this.popoversByTrigger.get(trigger);
|
|
}
|
|
|
|
const documentRef =
|
|
trigger?.ownerDocument ||
|
|
this.document ||
|
|
(typeof document !== "undefined" ? document : null);
|
|
|
|
const entry = {
|
|
trigger,
|
|
context: {
|
|
video: null,
|
|
pointerInfo: null,
|
|
playbackUrl: "",
|
|
playbackMagnet: "",
|
|
canManageBlacklist: false,
|
|
context: "card",
|
|
designSystem: this.designSystem,
|
|
onAction: null,
|
|
onClose: null,
|
|
restoreFocusOnClose: true,
|
|
},
|
|
popover: null,
|
|
};
|
|
|
|
const render = this.createPopoverRender(entry);
|
|
|
|
const popover = createPopover(trigger, render, {
|
|
document: documentRef,
|
|
placement: "bottom-end",
|
|
});
|
|
|
|
const decorated = this.decoratePopoverLifecycle(popover, trigger, entry) || popover;
|
|
entry.popover = decorated;
|
|
this.popoversByTrigger.set(trigger, entry);
|
|
return entry;
|
|
}
|
|
|
|
async toggleMoreMenu(detail = {}) {
|
|
const {
|
|
trigger = null,
|
|
video = null,
|
|
pointerInfo = null,
|
|
playbackUrl = "",
|
|
playbackMagnet = "",
|
|
context = "card",
|
|
canManageBlacklist = this.callbacks.canCurrentUserManageBlacklist(),
|
|
designSystem = null,
|
|
onAction = null,
|
|
onClose = null,
|
|
restoreFocusOnClose = true,
|
|
} = detail;
|
|
|
|
const entry = this.ensurePopoverForTrigger({ trigger });
|
|
if (!entry?.popover) {
|
|
return;
|
|
}
|
|
|
|
entry.context = {
|
|
...entry.context,
|
|
video,
|
|
pointerInfo,
|
|
playbackUrl,
|
|
playbackMagnet,
|
|
canManageBlacklist,
|
|
context,
|
|
designSystem: designSystem || this.designSystem,
|
|
onAction,
|
|
onClose,
|
|
restoreFocusOnClose: restoreFocusOnClose !== false,
|
|
};
|
|
|
|
if (typeof entry.popover.isOpen === "function" && entry.popover.isOpen()) {
|
|
entry.popover.close({
|
|
restoreFocus: entry.context.restoreFocusOnClose !== false,
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await entry.popover.open();
|
|
} catch (error) {
|
|
userLogger.error("[MoreMenu] Failed to open popover:", error);
|
|
}
|
|
}
|
|
|
|
closePopoverForTrigger(trigger, options = {}) {
|
|
const entry = trigger ? this.popoversByTrigger.get(trigger) : null;
|
|
if (!entry?.popover || typeof entry.popover.close !== "function") {
|
|
return false;
|
|
}
|
|
|
|
const restoreFocus =
|
|
options?.restoreFocus !== undefined
|
|
? options.restoreFocus
|
|
: entry.context?.restoreFocusOnClose !== false;
|
|
|
|
return entry.popover.close({ restoreFocus });
|
|
}
|
|
|
|
closeAllMoreMenus(options = {}) {
|
|
const skipView = options?.skipView === true;
|
|
const restoreFocus = options?.restoreFocus !== false;
|
|
|
|
if (!skipView && this.videoListView && typeof this.videoListView.closeAllMenus === "function") {
|
|
this.videoListView.closeAllMenus({
|
|
skipController: true,
|
|
restoreFocus,
|
|
});
|
|
}
|
|
|
|
this.popoversByTrigger.forEach((entry) => {
|
|
if (entry?.popover && typeof entry.popover.close === "function") {
|
|
entry.popover.close({ restoreFocus });
|
|
}
|
|
});
|
|
|
|
if (this.activePopover) {
|
|
this.activePopover = null;
|
|
}
|
|
}
|
|
|
|
attachMoreMenuHandlers(container) {
|
|
if (!container || typeof container.querySelectorAll !== "function") {
|
|
return;
|
|
}
|
|
|
|
if (!this.document) {
|
|
return;
|
|
}
|
|
|
|
const buttons = container.querySelectorAll("[data-more-dropdown]");
|
|
if (!buttons.length) {
|
|
return;
|
|
}
|
|
|
|
this.ensureGlobalMoreMenuHandlers();
|
|
|
|
const HTMLElementCtor = this.getHTMLElementConstructor(this.document);
|
|
|
|
buttons.forEach((button) => {
|
|
if (HTMLElementCtor && !(button instanceof HTMLElementCtor)) {
|
|
return;
|
|
}
|
|
if (button.dataset.moreMenuToggleBound === "true") {
|
|
return;
|
|
}
|
|
button.dataset.moreMenuToggleBound = "true";
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const context = button.getAttribute("data-context") || "card";
|
|
|
|
let video = null;
|
|
let pointerInfo = null;
|
|
let playbackUrl = "";
|
|
let playbackMagnet = "";
|
|
|
|
if (context === "modal") {
|
|
video = this.callbacks.getCurrentVideo();
|
|
if (Array.isArray(video?.pointer) && video.pointer.length >= 2) {
|
|
pointerInfo = { pointer: video.pointer };
|
|
}
|
|
playbackUrl = typeof video?.url === "string" ? video.url : "";
|
|
playbackMagnet = (() => {
|
|
if (typeof video?.magnet === "string" && video.magnet) {
|
|
return video.magnet;
|
|
}
|
|
if (typeof video?.infoHash === "string" && video.infoHash) {
|
|
return video.infoHash;
|
|
}
|
|
return "";
|
|
})();
|
|
}
|
|
|
|
if (!pointerInfo) {
|
|
const pointerType = button.getAttribute("data-pointer-type") || "";
|
|
const pointerValue = button.getAttribute("data-pointer-value") || "";
|
|
const pointerRelay = button.getAttribute("data-pointer-relay") || "";
|
|
const pointerKey = button.getAttribute("data-pointer-key") || "";
|
|
if (pointerType && pointerValue) {
|
|
pointerInfo = {
|
|
pointer: pointerRelay
|
|
? [pointerType, pointerValue, pointerRelay]
|
|
: [pointerType, pointerValue],
|
|
key: pointerKey,
|
|
};
|
|
}
|
|
}
|
|
|
|
this.toggleMoreMenu({
|
|
trigger: button,
|
|
video,
|
|
pointerInfo,
|
|
playbackUrl,
|
|
playbackMagnet,
|
|
context,
|
|
canManageBlacklist: this.callbacks.canCurrentUserManageBlacklist(),
|
|
designSystem: this.designSystem,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
syncModalMoreMenuData() {
|
|
if (!this.videoModal || typeof this.videoModal.syncMoreMenuData !== "function") {
|
|
return;
|
|
}
|
|
|
|
const currentVideo = this.callbacks.getCurrentVideo();
|
|
const canManageBlacklist = this.callbacks.canCurrentUserManageBlacklist();
|
|
|
|
this.videoModal.syncMoreMenuData({
|
|
currentVideo,
|
|
canManageBlacklist,
|
|
});
|
|
}
|
|
|
|
async handleMoreMenuAction(action, dataset = {}) {
|
|
const normalized = typeof action === "string" ? action.trim() : "";
|
|
const context = dataset.context || "";
|
|
const currentVideo = this.callbacks.getCurrentVideo();
|
|
|
|
switch (normalized) {
|
|
case "open-channel": {
|
|
if (context === "modal") {
|
|
this.callbacks.openCreatorChannel();
|
|
break;
|
|
}
|
|
|
|
const author =
|
|
dataset.author || (currentVideo ? currentVideo.pubkey : "");
|
|
if (author) {
|
|
this.callbacks.goToProfile(author);
|
|
} else {
|
|
this.callbacks.showError("No creator info available.");
|
|
}
|
|
break;
|
|
}
|
|
case "copy-link": {
|
|
const eventId =
|
|
dataset.eventId || (context === "modal" ? currentVideo?.id : "");
|
|
if (!eventId) {
|
|
this.callbacks.showError("Could not generate link.");
|
|
break;
|
|
}
|
|
const shareUrl = this.callbacks.buildShareUrlFromEventId(eventId);
|
|
if (!shareUrl) {
|
|
this.callbacks.showError("Could not generate link.");
|
|
break;
|
|
}
|
|
|
|
if (!this.clipboard || typeof this.clipboard.writeText !== "function") {
|
|
this.callbacks.showError("Failed to copy the link.");
|
|
break;
|
|
}
|
|
|
|
this.clipboard
|
|
.writeText(shareUrl)
|
|
.then(() =>
|
|
this.callbacks.showSuccess("Video link copied to clipboard!"),
|
|
)
|
|
.catch(() => this.callbacks.showError("Failed to copy the link."));
|
|
break;
|
|
}
|
|
case "remove-history": {
|
|
await this.callbacks.handleRemoveHistoryAction(dataset);
|
|
break;
|
|
}
|
|
case "repost-event": {
|
|
await this.callbacks.handleRepostAction(dataset);
|
|
break;
|
|
}
|
|
case "mirror-video": {
|
|
await this.callbacks.handleMirrorAction(dataset);
|
|
break;
|
|
}
|
|
case "ensure-presence": {
|
|
await this.callbacks.handleEnsurePresenceAction(dataset);
|
|
break;
|
|
}
|
|
case "copy-npub": {
|
|
const explicitNpub =
|
|
typeof dataset.npub === "string" && dataset.npub.trim()
|
|
? dataset.npub.trim()
|
|
: "";
|
|
const authorCandidate = dataset.author || "";
|
|
const fallbackNpub = this.callbacks.safeEncodeNpub(authorCandidate);
|
|
const valueToCopy = explicitNpub || fallbackNpub;
|
|
|
|
if (!valueToCopy) {
|
|
this.callbacks.showError("No npub available to copy.");
|
|
break;
|
|
}
|
|
|
|
if (!this.clipboard || typeof this.clipboard.writeText !== "function") {
|
|
this.callbacks.showError("Failed to copy the npub.");
|
|
break;
|
|
}
|
|
|
|
try {
|
|
await this.clipboard.writeText(valueToCopy);
|
|
this.callbacks.showSuccess("Channel npub copied to clipboard!");
|
|
} catch (error) {
|
|
userLogger.error("Failed to copy npub:", error);
|
|
this.callbacks.showError("Failed to copy the npub.");
|
|
}
|
|
break;
|
|
}
|
|
case "blacklist-author": {
|
|
const actorNpub = this.callbacks.getCurrentUserNpub();
|
|
if (!actorNpub) {
|
|
this.callbacks.showError(
|
|
"Please login as a moderator to manage the blacklist.",
|
|
);
|
|
break;
|
|
}
|
|
|
|
try {
|
|
await this.accessControl?.ensureReady?.();
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"Failed to refresh moderation state before blacklisting:",
|
|
error,
|
|
);
|
|
}
|
|
|
|
if (!this.accessControl?.canEditAdminLists?.(actorNpub)) {
|
|
this.callbacks.showError("Only moderators can manage the blacklist.");
|
|
break;
|
|
}
|
|
|
|
let author = dataset.author || "";
|
|
if (!author && context === "modal" && currentVideo?.pubkey) {
|
|
author = currentVideo.pubkey;
|
|
}
|
|
if (!author && currentVideo?.pubkey) {
|
|
author = currentVideo.pubkey;
|
|
}
|
|
|
|
const explicitNpub =
|
|
typeof dataset.npub === "string" && dataset.npub.trim()
|
|
? dataset.npub.trim()
|
|
: "";
|
|
const targetNpub = explicitNpub || this.callbacks.safeEncodeNpub(author);
|
|
|
|
if (!targetNpub) {
|
|
this.callbacks.showError("Unable to determine the creator npub.");
|
|
break;
|
|
}
|
|
|
|
try {
|
|
const result = await this.accessControl.addToBlacklist(
|
|
actorNpub,
|
|
targetNpub,
|
|
);
|
|
|
|
if (result?.ok) {
|
|
this.callbacks.showSuccess("Creator added to the blacklist.");
|
|
try {
|
|
await this.subscriptions?.refreshActiveFeed?.({
|
|
reason: "admin-blacklist-update",
|
|
});
|
|
} catch (error) {
|
|
if (this.isDevMode) {
|
|
userLogger.warn(
|
|
"[Subscriptions] Failed to refresh after blacklist update:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
const code = result?.error || "unknown";
|
|
switch (code) {
|
|
case "self":
|
|
this.callbacks.showError("You cannot blacklist yourself.");
|
|
break;
|
|
case "immutable":
|
|
this.callbacks.showError(
|
|
"Moderators cannot blacklist the super admin or fellow moderators.",
|
|
);
|
|
break;
|
|
case "invalid npub":
|
|
this.callbacks.showError("Unable to blacklist this creator.");
|
|
break;
|
|
case "forbidden":
|
|
this.callbacks.showError("Only moderators can manage the blacklist.");
|
|
break;
|
|
default:
|
|
this.callbacks.showError(
|
|
"Failed to update the blacklist. Please try again.",
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
userLogger.error("Failed to add creator to blacklist:", error);
|
|
this.callbacks.showError(
|
|
"Failed to update the blacklist. Please try again.",
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "block-author": {
|
|
const currentUserPubkey = this.callbacks.getCurrentUserPubkey();
|
|
if (!currentUserPubkey) {
|
|
this.callbacks.showError("Please login to manage your block list.");
|
|
break;
|
|
}
|
|
|
|
const authorCandidate =
|
|
dataset.author || (currentVideo && currentVideo.pubkey) || "";
|
|
|
|
const trimmed =
|
|
typeof authorCandidate === "string" ? authorCandidate.trim() : "";
|
|
if (!trimmed) {
|
|
this.callbacks.showError("Unable to determine the creator to block.");
|
|
break;
|
|
}
|
|
|
|
let normalizedHex = "";
|
|
if (trimmed.startsWith("npub1")) {
|
|
normalizedHex = this.callbacks.safeDecodeNpub(trimmed) || "";
|
|
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
|
normalizedHex = trimmed.toLowerCase();
|
|
}
|
|
|
|
if (!normalizedHex) {
|
|
this.callbacks.showError("Unable to determine the creator to block.");
|
|
break;
|
|
}
|
|
|
|
if (normalizedHex === currentUserPubkey) {
|
|
this.callbacks.showError("You cannot block yourself.");
|
|
break;
|
|
}
|
|
|
|
try {
|
|
await this.userBlocks?.ensureLoaded?.(currentUserPubkey);
|
|
|
|
if (this.userBlocks?.isBlocked?.(normalizedHex)) {
|
|
this.callbacks.showSuccess("You already blocked this creator.");
|
|
} else {
|
|
await this.userBlocks?.addBlock?.(normalizedHex, currentUserPubkey);
|
|
this.callbacks.showSuccess(
|
|
"Creator blocked. You won't see their videos anymore.",
|
|
);
|
|
}
|
|
|
|
try {
|
|
this.callbacks.onUserBlocksUpdated();
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"[MoreMenu] Failed to refresh blocked list after update:",
|
|
error,
|
|
);
|
|
}
|
|
|
|
await this.callbacks.loadVideos();
|
|
try {
|
|
await this.subscriptions?.refreshActiveFeed?.({
|
|
reason: "user-block-update",
|
|
});
|
|
} catch (error) {
|
|
if (this.isDevMode) {
|
|
userLogger.warn(
|
|
"[Subscriptions] Failed to refresh after user block update:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
userLogger.error("Failed to update personal block list:", error);
|
|
const message =
|
|
error?.code === "nip04-missing"
|
|
? "Your Nostr extension must support NIP-04 to manage private lists."
|
|
: "Failed to update your block list. Please try again.";
|
|
this.callbacks.showError(message);
|
|
}
|
|
break;
|
|
}
|
|
case "report": {
|
|
this.callbacks.showSuccess("Reporting coming soon.");
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|