mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Remove development comment from channel profile import
This commit is contained in:
1 parent
8492091780
commit
fda1726769
6 files changed
+760
-10
No files matched your search
+42
-1
@@ -5,7 +5,7 @@ import {
|
||||
DEFAULT_RELAY_URLS,
|
||||
convertEventToVideo as sharedConvertEventToVideo
|
||||
} from "./nostr.js";
|
||||
import { subscriptions } from "./subscriptions.js"; // <-- NEW import
|
||||
import { subscriptions } from "./subscriptions.js";
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import { accessControl } from "./accessControl.js";
|
||||
@@ -16,6 +16,7 @@ import { VideoCard } from "./ui/components/VideoCard.js";
|
||||
import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderers.js";
|
||||
import { ALLOW_NSFW_CONTENT } from "./config.js";
|
||||
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
||||
import moderationService from "./services/moderationService.js";
|
||||
import {
|
||||
calculateZapShares,
|
||||
describeShareType,
|
||||
@@ -1663,12 +1664,31 @@ async function updateChannelMenuState() {
|
||||
userLogger.warn("Failed to refresh moderation lists for channel menu:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
await moderationService.ensureViewerMuteListLoaded();
|
||||
} catch (error) {
|
||||
userLogger.warn("Failed to refresh viewer mute list for channel menu:", error);
|
||||
}
|
||||
|
||||
const canBlacklist =
|
||||
typeof app?.canCurrentUserManageBlacklist === "function"
|
||||
? app.canCurrentUserManageBlacklist()
|
||||
: false;
|
||||
|
||||
const buttons = menu.querySelectorAll("button[data-action]");
|
||||
const toggleButtonVisibility = (button, shouldShow) => {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
if (shouldShow) {
|
||||
button.removeAttribute("hidden");
|
||||
button.setAttribute("aria-hidden", "false");
|
||||
} else {
|
||||
button.setAttribute("hidden", "");
|
||||
button.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
};
|
||||
|
||||
buttons.forEach((button) => {
|
||||
if (!button || button.nodeType !== 1) {
|
||||
return;
|
||||
@@ -1701,6 +1721,27 @@ async function updateChannelMenuState() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "mute-author" || action === "unmute-author") {
|
||||
if (currentChannelHex) {
|
||||
button.dataset.author = currentChannelHex;
|
||||
} else {
|
||||
delete button.dataset.author;
|
||||
}
|
||||
|
||||
let isMuted = false;
|
||||
if (currentChannelHex) {
|
||||
try {
|
||||
isMuted = moderationService.isAuthorMutedByViewer(currentChannelHex) === true;
|
||||
} catch (error) {
|
||||
userLogger.warn("Failed to resolve viewer mute state for channel menu:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldShow = action === "mute-author" ? !isMuted : isMuted;
|
||||
toggleButtonVisibility(button, shouldShow && !!currentChannelHex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "blacklist-author") {
|
||||
if (canBlacklist && currentChannelHex) {
|
||||
button.dataset.author = currentChannelHex;
|
||||
|
||||
@@ -319,6 +319,11 @@ export class ModerationService {
|
||||
this.userBlocks = userBlockManager;
|
||||
this.accessControl = accessControlService;
|
||||
|
||||
this.viewerMuteList = new Set();
|
||||
this.viewerMuteEventId = "";
|
||||
this.viewerMuteUpdatedAt = 0;
|
||||
this.viewerMutePromise = null;
|
||||
|
||||
this.contactSubscription = null;
|
||||
this.contactListPromise = null;
|
||||
|
||||
@@ -391,13 +396,34 @@ export class ModerationService {
|
||||
|
||||
this.trustedMuteLists.clear();
|
||||
this.trustedMutedAuthors.clear();
|
||||
this.viewerMuteList.clear();
|
||||
this.viewerMuteEventId = "";
|
||||
this.viewerMuteUpdatedAt = 0;
|
||||
|
||||
this.emit("trusted-mutes", { total: 0 });
|
||||
}
|
||||
|
||||
isTrustedMuteOwner(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (normalized === this.viewerPubkey) {
|
||||
return true;
|
||||
}
|
||||
return this.trustedContacts.has(normalized);
|
||||
}
|
||||
|
||||
reconcileTrustedMuteSubscriptions(previousSet, nextSet) {
|
||||
const previous = previousSet instanceof Set ? previousSet : new Set();
|
||||
const next = nextSet instanceof Set ? nextSet : new Set();
|
||||
const previous = previousSet instanceof Set ? new Set(previousSet) : new Set();
|
||||
const next = nextSet instanceof Set ? new Set(nextSet) : new Set();
|
||||
|
||||
if (this.viewerPubkey) {
|
||||
if (this.trustedMuteSubscriptions.has(this.viewerPubkey)) {
|
||||
previous.add(this.viewerPubkey);
|
||||
}
|
||||
next.add(this.viewerPubkey);
|
||||
}
|
||||
|
||||
for (const value of previous) {
|
||||
if (!next.has(value)) {
|
||||
@@ -436,7 +462,7 @@ export class ModerationService {
|
||||
|
||||
async subscribeToTrustedMuteList(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized || !this.trustedContacts.has(normalized)) {
|
||||
if (!normalized || !this.isTrustedMuteOwner(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -466,7 +492,7 @@ export class ModerationService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.trustedContacts.has(normalized)) {
|
||||
if (!this.isTrustedMuteOwner(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -501,9 +527,11 @@ export class ModerationService {
|
||||
if (latest) {
|
||||
this.ingestTrustedMuteEvent(latest);
|
||||
}
|
||||
} else {
|
||||
this.replaceTrustedMuteList(normalized, new Set(), { createdAt: 0, eventId: "" });
|
||||
}
|
||||
|
||||
if (!this.trustedContacts.has(normalized)) {
|
||||
if (!this.isTrustedMuteOwner(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -590,13 +618,22 @@ export class ModerationService {
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedEventId = normalizeEventId(eventId);
|
||||
const normalizedCreatedAt = ensureNumber(createdAt);
|
||||
|
||||
if (owner === this.viewerPubkey) {
|
||||
this.viewerMuteList = new Set(sanitizedAuthors);
|
||||
this.viewerMuteEventId = normalizedEventId || "";
|
||||
this.viewerMuteUpdatedAt = normalizedCreatedAt;
|
||||
}
|
||||
|
||||
if (!sanitizedAuthors.size) {
|
||||
this.trustedMuteLists.delete(owner);
|
||||
} else {
|
||||
this.trustedMuteLists.set(owner, {
|
||||
authors: sanitizedAuthors,
|
||||
updatedAt: ensureNumber(createdAt),
|
||||
eventId: normalizeEventId(eventId),
|
||||
updatedAt: normalizedCreatedAt,
|
||||
eventId: normalizedEventId,
|
||||
});
|
||||
|
||||
for (const author of sanitizedAuthors) {
|
||||
@@ -615,7 +652,7 @@ export class ModerationService {
|
||||
|
||||
applyTrustedMuteEvent(ownerPubkey, event) {
|
||||
const owner = normalizeHex(ownerPubkey);
|
||||
if (!owner || !this.trustedContacts.has(owner)) {
|
||||
if (!owner || !this.isTrustedMuteOwner(owner)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -663,13 +700,202 @@ export class ModerationService {
|
||||
}
|
||||
|
||||
const owner = normalizeHex(event.pubkey);
|
||||
if (!owner || !this.trustedContacts.has(owner)) {
|
||||
if (!owner || !this.isTrustedMuteOwner(owner)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyTrustedMuteEvent(owner, event);
|
||||
}
|
||||
|
||||
getViewerMutedAuthors() {
|
||||
return Array.from(this.viewerMuteList);
|
||||
}
|
||||
|
||||
isAuthorMutedByViewer(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return this.viewerMuteList.has(normalized);
|
||||
}
|
||||
|
||||
async ensureViewerMuteListLoaded(pubkey = this.viewerPubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
this.viewerMuteList.clear();
|
||||
this.viewerMuteEventId = "";
|
||||
this.viewerMuteUpdatedAt = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.trustedMuteLists.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.viewerMutePromise) {
|
||||
try {
|
||||
await this.viewerMutePromise;
|
||||
} catch (_) {
|
||||
/* noop */
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewerMutePromise = this.subscribeToTrustedMuteList(normalized)
|
||||
.catch((error) => {
|
||||
this.log("[moderationService] failed to load viewer mute list", error);
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
this.viewerMutePromise = null;
|
||||
});
|
||||
|
||||
try {
|
||||
await this.viewerMutePromise;
|
||||
} catch (_) {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
async publishViewerMuteList({ owner, muted }) {
|
||||
const viewer = normalizeHex(owner || this.viewerPubkey);
|
||||
if (!viewer) {
|
||||
const error = new Error("viewer-not-logged-in");
|
||||
error.code = "viewer-not-logged-in";
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.ensurePool();
|
||||
|
||||
const extension = typeof window !== "undefined" ? window.nostr : null;
|
||||
if (!extension || typeof extension.signEvent !== "function") {
|
||||
const error = new Error("nostr-extension-missing");
|
||||
error.code = "nostr-extension-missing";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (typeof this.nostrClient?.ensureExtensionPermissions === "function") {
|
||||
const permissionResult = await this.nostrClient.ensureExtensionPermissions([
|
||||
"sign_event",
|
||||
"get_public_key",
|
||||
]);
|
||||
if (!permissionResult?.ok) {
|
||||
const error = new Error("extension-permission-denied");
|
||||
error.code = "extension-permission-denied";
|
||||
error.details = permissionResult?.error || null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const tags = [];
|
||||
if (muted instanceof Set || Array.isArray(muted)) {
|
||||
for (const value of muted) {
|
||||
const normalized = normalizeHex(value);
|
||||
if (!normalized || normalized === viewer) {
|
||||
continue;
|
||||
}
|
||||
tags.push(["p", normalized]);
|
||||
}
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 10000,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: "",
|
||||
pubkey: viewer,
|
||||
};
|
||||
|
||||
let signedEvent;
|
||||
try {
|
||||
signedEvent = await extension.signEvent(event);
|
||||
} catch (error) {
|
||||
const wrapped = new Error("signature-failed");
|
||||
wrapped.code = "signature-failed";
|
||||
wrapped.details = error;
|
||||
throw wrapped;
|
||||
}
|
||||
|
||||
const relays = resolveRelayList(this.nostrClient, { write: true });
|
||||
if (!relays.length) {
|
||||
const error = new Error("no-relays-configured");
|
||||
error.code = "no-relays-configured";
|
||||
throw error;
|
||||
}
|
||||
|
||||
let results = [];
|
||||
try {
|
||||
results = await publishEventToRelays(this.nostrClient.pool, relays, signedEvent);
|
||||
assertAnyRelayAccepted(results, { context: "mute list" });
|
||||
} catch (error) {
|
||||
const wrapped = new Error("mute-list-publish-failed");
|
||||
wrapped.code = "mute-list-publish-failed";
|
||||
wrapped.details = error;
|
||||
throw wrapped;
|
||||
}
|
||||
|
||||
this.applyTrustedMuteEvent(viewer, signedEvent);
|
||||
return { ok: true, event: signedEvent, results };
|
||||
}
|
||||
|
||||
async addAuthorToViewerMuteList(pubkey) {
|
||||
const viewer = normalizeHex(this.viewerPubkey);
|
||||
if (!viewer) {
|
||||
const error = new Error("viewer-not-logged-in");
|
||||
error.code = "viewer-not-logged-in";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const target = normalizeHex(pubkey);
|
||||
if (!target) {
|
||||
const error = new Error("invalid-target");
|
||||
error.code = "invalid-target";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (target === viewer) {
|
||||
const error = new Error("self");
|
||||
error.code = "self";
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.ensureViewerMuteListLoaded(viewer);
|
||||
|
||||
if (this.viewerMuteList.has(target)) {
|
||||
return { ok: true, already: true };
|
||||
}
|
||||
|
||||
const next = new Set(this.viewerMuteList);
|
||||
next.add(target);
|
||||
|
||||
return this.publishViewerMuteList({ owner: viewer, muted: next });
|
||||
}
|
||||
|
||||
async removeAuthorFromViewerMuteList(pubkey) {
|
||||
const viewer = normalizeHex(this.viewerPubkey);
|
||||
if (!viewer) {
|
||||
const error = new Error("viewer-not-logged-in");
|
||||
error.code = "viewer-not-logged-in";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const target = normalizeHex(pubkey);
|
||||
if (!target) {
|
||||
return { ok: true, already: true };
|
||||
}
|
||||
|
||||
await this.ensureViewerMuteListLoaded(viewer);
|
||||
|
||||
if (!this.viewerMuteList.has(target)) {
|
||||
return { ok: true, already: true };
|
||||
}
|
||||
|
||||
const next = new Set(this.viewerMuteList);
|
||||
next.delete(target);
|
||||
|
||||
return this.publishViewerMuteList({ owner: viewer, muted: next });
|
||||
}
|
||||
|
||||
isAuthorMutedByTrusted(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
@@ -910,6 +1136,7 @@ export class ModerationService {
|
||||
});
|
||||
|
||||
await this.ensureUserBlocksLoaded(normalized);
|
||||
await this.ensureViewerMuteListLoaded(normalized);
|
||||
return this.viewerPubkey;
|
||||
}
|
||||
|
||||
|
||||
@@ -257,6 +257,18 @@ export function createVideoMoreMenuPanel({
|
||||
});
|
||||
}
|
||||
|
||||
appendMenuAction(doc, list, {
|
||||
text: "Mute creator",
|
||||
action: "mute-author",
|
||||
dataset: { author: metadata.pubkey, context },
|
||||
});
|
||||
|
||||
appendMenuAction(doc, list, {
|
||||
text: "Unmute creator",
|
||||
action: "unmute-author",
|
||||
dataset: { author: metadata.pubkey, context },
|
||||
});
|
||||
|
||||
if (canManageBlacklist) {
|
||||
appendMenuAction(doc, list, {
|
||||
text: "Blacklist creator",
|
||||
@@ -307,6 +319,18 @@ export function createChannelProfileMenuPanel({
|
||||
dataset: { context: normalizedContext },
|
||||
});
|
||||
|
||||
appendMenuAction(doc, list, {
|
||||
text: "Mute channel",
|
||||
action: "mute-author",
|
||||
dataset: { context: normalizedContext },
|
||||
});
|
||||
|
||||
appendMenuAction(doc, list, {
|
||||
text: "Unmute channel",
|
||||
action: "unmute-author",
|
||||
dataset: { context: normalizedContext },
|
||||
});
|
||||
|
||||
appendMenuAction(doc, list, {
|
||||
text: "Blacklist channel",
|
||||
action: "blacklist-author",
|
||||
|
||||
@@ -79,6 +79,7 @@ export default class MoreMenuController {
|
||||
this.boundVideoListBlacklistHandler = null;
|
||||
this.popoversByTrigger = new Map();
|
||||
this.activePopover = null;
|
||||
this.activePopoverEntry = null;
|
||||
|
||||
this.reportModal = null;
|
||||
this.reportModalPrepared = false;
|
||||
@@ -582,6 +583,105 @@ export default class MoreMenuController {
|
||||
);
|
||||
}
|
||||
|
||||
setButtonVisibility(button, visible) {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
button.removeAttribute("hidden");
|
||||
button.setAttribute("aria-hidden", "false");
|
||||
} else {
|
||||
button.setAttribute("hidden", "");
|
||||
button.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
|
||||
syncMuteMenuButtons(panel, entry = {}) {
|
||||
if (!panel || typeof panel.querySelector !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const muteButton = panel.querySelector('button[data-action="mute-author"]');
|
||||
const unmuteButton = panel.querySelector('button[data-action="unmute-author"]');
|
||||
if (!muteButton && !unmuteButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contextVideo = entry?.context?.video || null;
|
||||
let authorCandidate = contextVideo?.pubkey || "";
|
||||
|
||||
if (!authorCandidate) {
|
||||
if (muteButton?.dataset?.author) {
|
||||
authorCandidate = muteButton.dataset.author;
|
||||
} else if (unmuteButton?.dataset?.author) {
|
||||
authorCandidate = unmuteButton.dataset.author;
|
||||
}
|
||||
}
|
||||
|
||||
const trimmed = typeof authorCandidate === "string" ? authorCandidate.trim() : "";
|
||||
let normalized = "";
|
||||
if (trimmed) {
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
normalized = trimmed.toLowerCase();
|
||||
} else if (trimmed.startsWith("npub1")) {
|
||||
normalized = this.callbacks.safeDecodeNpub(trimmed) || "";
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized) {
|
||||
if (muteButton) {
|
||||
muteButton.dataset.author = normalized;
|
||||
}
|
||||
if (unmuteButton) {
|
||||
unmuteButton.dataset.author = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
let isMuted = false;
|
||||
if (normalized) {
|
||||
try {
|
||||
isMuted = this.moderationService.isAuthorMutedByViewer(normalized) === true;
|
||||
} catch (error) {
|
||||
if (this.isDevMode) {
|
||||
userLogger.warn("[MoreMenu] Failed to resolve viewer mute state", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasTarget = Boolean(normalized);
|
||||
this.setButtonVisibility(muteButton, hasTarget && !isMuted);
|
||||
this.setButtonVisibility(unmuteButton, hasTarget && isMuted);
|
||||
}
|
||||
|
||||
refreshActiveMuteButtons({ author = "" } = {}) {
|
||||
if (!this.activePopoverEntry || !this.activePopover) {
|
||||
return;
|
||||
}
|
||||
const panel =
|
||||
typeof this.activePopover.getPanel === "function"
|
||||
? this.activePopover.getPanel()
|
||||
: null;
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (author) {
|
||||
const trimmed = author.trim();
|
||||
if (trimmed) {
|
||||
const muteButton = panel.querySelector('button[data-action="mute-author"]');
|
||||
const unmuteButton = panel.querySelector('button[data-action="unmute-author"]');
|
||||
if (muteButton) {
|
||||
muteButton.dataset.author = trimmed;
|
||||
}
|
||||
if (unmuteButton) {
|
||||
unmuteButton.dataset.author = trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.syncMuteMenuButtons(panel, this.activePopoverEntry);
|
||||
}
|
||||
|
||||
createPopoverRender(entry) {
|
||||
return ({ document: documentRef, close }) => {
|
||||
const panel = createVideoMoreMenuPanel({
|
||||
@@ -599,6 +699,8 @@ export default class MoreMenuController {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.syncMuteMenuButtons(panel, entry);
|
||||
|
||||
const buttons = panel.querySelectorAll("button[data-action]");
|
||||
buttons.forEach((button) => {
|
||||
const HTMLElementCtor = this.getHTMLElementConstructor(button?.ownerDocument);
|
||||
@@ -656,6 +758,12 @@ export default class MoreMenuController {
|
||||
const result = await originalOpen(...args);
|
||||
if (result) {
|
||||
this.activePopover = popover;
|
||||
this.activePopoverEntry = entry;
|
||||
const panel =
|
||||
typeof popover.getPanel === "function" ? popover.getPanel() : null;
|
||||
if (panel) {
|
||||
this.syncMuteMenuButtons(panel, entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -670,6 +778,7 @@ export default class MoreMenuController {
|
||||
const result = originalClose({ restoreFocus });
|
||||
if (result && this.activePopover === popover) {
|
||||
this.activePopover = null;
|
||||
this.activePopoverEntry = null;
|
||||
}
|
||||
if (result && typeof entry?.context?.onClose === "function") {
|
||||
try {
|
||||
@@ -692,6 +801,7 @@ export default class MoreMenuController {
|
||||
popover.destroy = (...args) => {
|
||||
if (this.activePopover === popover) {
|
||||
this.activePopover = null;
|
||||
this.activePopoverEntry = null;
|
||||
}
|
||||
originalDestroy(...args);
|
||||
if (trigger && this.popoversByTrigger.get(trigger) === entry) {
|
||||
@@ -942,6 +1052,36 @@ export default class MoreMenuController {
|
||||
const context = dataset.context || "";
|
||||
const currentVideo = this.callbacks.getCurrentVideo();
|
||||
|
||||
const resolveTargetAuthorHex = () => {
|
||||
const candidates = [
|
||||
dataset.author,
|
||||
dataset.npub,
|
||||
context === "modal" ? currentVideo?.pubkey : "",
|
||||
currentVideo?.pubkey,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.callbacks.safeDecodeNpub(trimmed);
|
||||
if (decoded) {
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
switch (normalized) {
|
||||
case "open-channel": {
|
||||
if (context === "modal") {
|
||||
@@ -1028,6 +1168,156 @@ export default class MoreMenuController {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mute-author": {
|
||||
const viewerPubkey = this.callbacks.getCurrentUserPubkey();
|
||||
if (!viewerPubkey) {
|
||||
this.callbacks.showError("Please login to manage your mute list.");
|
||||
break;
|
||||
}
|
||||
|
||||
const targetHex = resolveTargetAuthorHex();
|
||||
if (!targetHex) {
|
||||
this.callbacks.showError("Unable to determine the creator to mute.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetHex === viewerPubkey) {
|
||||
this.callbacks.showError("You cannot mute yourself.");
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.moderationService.addAuthorToViewerMuteList(
|
||||
targetHex,
|
||||
);
|
||||
if (result?.already) {
|
||||
this.callbacks.showSuccess("You already muted this creator.");
|
||||
} else {
|
||||
this.callbacks.showSuccess(
|
||||
"Creator muted. Their videos will be downranked in your feeds.",
|
||||
);
|
||||
}
|
||||
try {
|
||||
await this.callbacks.loadVideos();
|
||||
} catch (loadError) {
|
||||
if (this.isDevMode) {
|
||||
userLogger.warn("[MoreMenu] Failed to reload videos after muting", loadError);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.subscriptions?.refreshActiveFeed?.({
|
||||
reason: "viewer-mute-update",
|
||||
});
|
||||
} catch (refreshError) {
|
||||
if (this.isDevMode) {
|
||||
userLogger.warn(
|
||||
"[Subscriptions] Failed to refresh after viewer mute update:",
|
||||
refreshError,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.refreshActiveMuteButtons({ author: targetHex });
|
||||
} catch (error) {
|
||||
switch (error?.code) {
|
||||
case "viewer-not-logged-in":
|
||||
this.callbacks.showError("Please login to manage your mute list.");
|
||||
break;
|
||||
case "invalid-target":
|
||||
this.callbacks.showError("Unable to determine the creator to mute.");
|
||||
break;
|
||||
case "self":
|
||||
this.callbacks.showError("You cannot mute yourself.");
|
||||
break;
|
||||
case "nostr-extension-missing":
|
||||
this.callbacks.showError(
|
||||
"A Nostr extension is required to update your mute list.",
|
||||
);
|
||||
break;
|
||||
case "extension-permission-denied":
|
||||
this.callbacks.showError(
|
||||
"Allow your Nostr extension to sign events before muting creators.",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
this.callbacks.showError(
|
||||
"Failed to update your mute list. Please try again.",
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "unmute-author": {
|
||||
const viewerPubkey = this.callbacks.getCurrentUserPubkey();
|
||||
if (!viewerPubkey) {
|
||||
this.callbacks.showError("Please login to manage your mute list.");
|
||||
break;
|
||||
}
|
||||
|
||||
const targetHex = resolveTargetAuthorHex();
|
||||
if (!targetHex) {
|
||||
this.callbacks.showError("Unable to determine the creator to unmute.");
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.moderationService.removeAuthorFromViewerMuteList(
|
||||
targetHex,
|
||||
);
|
||||
if (result?.already) {
|
||||
this.callbacks.showSuccess("This creator is not on your mute list.");
|
||||
} else {
|
||||
this.callbacks.showSuccess(
|
||||
"Creator removed from your mute list.",
|
||||
);
|
||||
}
|
||||
try {
|
||||
await this.callbacks.loadVideos();
|
||||
} catch (loadError) {
|
||||
if (this.isDevMode) {
|
||||
userLogger.warn(
|
||||
"[MoreMenu] Failed to reload videos after unmuting",
|
||||
loadError,
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.subscriptions?.refreshActiveFeed?.({
|
||||
reason: "viewer-mute-update",
|
||||
});
|
||||
} catch (refreshError) {
|
||||
if (this.isDevMode) {
|
||||
userLogger.warn(
|
||||
"[Subscriptions] Failed to refresh after viewer mute removal:",
|
||||
refreshError,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.refreshActiveMuteButtons({ author: targetHex });
|
||||
} catch (error) {
|
||||
switch (error?.code) {
|
||||
case "viewer-not-logged-in":
|
||||
this.callbacks.showError("Please login to manage your mute list.");
|
||||
break;
|
||||
case "nostr-extension-missing":
|
||||
this.callbacks.showError(
|
||||
"A Nostr extension is required to update your mute list.",
|
||||
);
|
||||
break;
|
||||
case "extension-permission-denied":
|
||||
this.callbacks.showError(
|
||||
"Allow your Nostr extension to sign events before updating your mute list.",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
this.callbacks.showError(
|
||||
"Failed to update your mute list. Please try again.",
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "blacklist-author": {
|
||||
const actorNpub = this.callbacks.getCurrentUserNpub();
|
||||
if (!actorNpub) {
|
||||
|
||||
@@ -157,3 +157,71 @@ test("trusted mute aggregation tracks F1 mute lists", () => {
|
||||
assert.equal(service.isAuthorMutedByTrusted(mutedAuthor), false);
|
||||
assert.deepEqual(service.getTrustedMutersForAuthor(mutedAuthor), []);
|
||||
});
|
||||
|
||||
test("viewer mute list publishes and updates aggregation", async (t) => {
|
||||
const publishCalls = [];
|
||||
const nostrClient = {
|
||||
pool: {
|
||||
list: async () => [],
|
||||
sub: () => ({
|
||||
on: () => {},
|
||||
unsub: () => {},
|
||||
}),
|
||||
publish: (urls, event) => {
|
||||
publishCalls.push({ urls, event });
|
||||
return {
|
||||
on: (eventName, handler) => {
|
||||
if (eventName === "ok") {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
relays: ["wss://relay.example"],
|
||||
ensurePool: async () => {},
|
||||
ensureExtensionPermissions: async () => ({ ok: true }),
|
||||
};
|
||||
|
||||
const service = new ModerationService({
|
||||
logger: () => {},
|
||||
nostrClient,
|
||||
});
|
||||
|
||||
const previousNostr = globalThis.window.nostr;
|
||||
globalThis.window.nostr = {
|
||||
async signEvent(event) {
|
||||
return { ...event, id: "signed-event-id" };
|
||||
},
|
||||
};
|
||||
|
||||
t.after(() => {
|
||||
globalThis.window.nostr = previousNostr;
|
||||
});
|
||||
|
||||
const viewerHex = "f".repeat(64);
|
||||
await service.setViewerPubkey(viewerHex);
|
||||
|
||||
const targetHex = "e".repeat(64);
|
||||
|
||||
await service.addAuthorToViewerMuteList(targetHex);
|
||||
|
||||
assert.equal(service.isAuthorMutedByViewer(targetHex), true);
|
||||
assert.equal(service.isAuthorMutedByTrusted(targetHex), true);
|
||||
assert.deepEqual(service.getTrustedMutersForAuthor(targetHex), [viewerHex]);
|
||||
|
||||
assert.equal(publishCalls.length, 1);
|
||||
const firstEvent = publishCalls[0].event;
|
||||
assert.equal(firstEvent.tags.length, 1);
|
||||
assert.deepEqual(firstEvent.tags[0], ["p", targetHex]);
|
||||
|
||||
await service.removeAuthorFromViewerMuteList(targetHex);
|
||||
|
||||
assert.equal(service.isAuthorMutedByViewer(targetHex), false);
|
||||
assert.equal(service.isAuthorMutedByTrusted(targetHex), false);
|
||||
assert.deepEqual(service.getTrustedMutersForAuthor(targetHex), []);
|
||||
|
||||
assert.equal(publishCalls.length, 2);
|
||||
const secondEvent = publishCalls[1].event;
|
||||
assert.equal(secondEvent.tags.length, 0);
|
||||
});
|
||||
@@ -159,3 +159,103 @@ test('block-author updates user blocks, reloads videos, and refreshes feeds', as
|
||||
['refreshActiveFeed', { reason: 'user-block-update' }],
|
||||
]);
|
||||
});
|
||||
|
||||
test('mute-author updates viewer mute list and refreshes feeds', async () => {
|
||||
const events = [];
|
||||
const moderationStub = {
|
||||
addAuthorToViewerMuteList: async (target) => {
|
||||
events.push(['addMute', target]);
|
||||
return { ok: true };
|
||||
},
|
||||
removeAuthorFromViewerMuteList: async () => {
|
||||
throw new Error('unreachable');
|
||||
},
|
||||
isAuthorMutedByViewer: () => false,
|
||||
};
|
||||
|
||||
const subscriptions = {
|
||||
refreshActiveFeed: async (args) => {
|
||||
events.push(['refreshActiveFeed', args]);
|
||||
},
|
||||
};
|
||||
|
||||
const controller = new MoreMenuController({
|
||||
moderationService: moderationStub,
|
||||
subscriptions,
|
||||
callbacks: {
|
||||
getCurrentUserPubkey: () => 'viewerhex',
|
||||
getCurrentVideo: () => ({ pubkey: 'fallbackhex' }),
|
||||
loadVideos: async () => {
|
||||
events.push(['loadVideos']);
|
||||
},
|
||||
showSuccess: (message) => {
|
||||
events.push(['success', message]);
|
||||
},
|
||||
showError: (message) => {
|
||||
events.push(['error', message]);
|
||||
},
|
||||
safeDecodeNpub: () => '',
|
||||
},
|
||||
});
|
||||
|
||||
const targetHex = 'd'.repeat(64);
|
||||
|
||||
await controller.handleMoreMenuAction('mute-author', { author: targetHex });
|
||||
|
||||
assert.deepEqual(events, [
|
||||
['addMute', targetHex],
|
||||
['success', 'Creator muted. Their videos will be downranked in your feeds.'],
|
||||
['loadVideos'],
|
||||
['refreshActiveFeed', { reason: 'viewer-mute-update' }],
|
||||
]);
|
||||
});
|
||||
|
||||
test('unmute-author removes creators from viewer mute list', async () => {
|
||||
const events = [];
|
||||
const moderationStub = {
|
||||
addAuthorToViewerMuteList: async () => {
|
||||
throw new Error('unreachable');
|
||||
},
|
||||
removeAuthorFromViewerMuteList: async (target) => {
|
||||
events.push(['removeMute', target]);
|
||||
return { ok: true };
|
||||
},
|
||||
isAuthorMutedByViewer: () => true,
|
||||
};
|
||||
|
||||
const subscriptions = {
|
||||
refreshActiveFeed: async (args) => {
|
||||
events.push(['refreshActiveFeed', args]);
|
||||
},
|
||||
};
|
||||
|
||||
const controller = new MoreMenuController({
|
||||
moderationService: moderationStub,
|
||||
subscriptions,
|
||||
callbacks: {
|
||||
getCurrentUserPubkey: () => 'viewerhex',
|
||||
getCurrentVideo: () => ({ pubkey: 'fallbackhex' }),
|
||||
loadVideos: async () => {
|
||||
events.push(['loadVideos']);
|
||||
},
|
||||
showSuccess: (message) => {
|
||||
events.push(['success', message]);
|
||||
},
|
||||
showError: (message) => {
|
||||
events.push(['error', message]);
|
||||
},
|
||||
safeDecodeNpub: () => '',
|
||||
},
|
||||
});
|
||||
|
||||
const targetHex = 'e'.repeat(64);
|
||||
|
||||
await controller.handleMoreMenuAction('unmute-author', { author: targetHex });
|
||||
|
||||
assert.deepEqual(events, [
|
||||
['removeMute', targetHex],
|
||||
['success', 'Creator removed from your mute list.'],
|
||||
['loadVideos'],
|
||||
['refreshActiveFeed', { reason: 'viewer-mute-update' }],
|
||||
]);
|
||||
});
|
||||
Reference in new issue
Block a user