Merge pull request #1270 from PR0M3TH3AN/codex/enhance-state-identifiers-and-render-overrides

Author-aware moderation overrides and profile modal UI for active overrides
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-16 13:52:25 -05:00
commit fba936635e
6 files changed
+467 -41

No files matched your search

+19
View File
@@ -414,6 +414,25 @@
role="status"
aria-live="polite"
></p>
<div class="space-y-3 border-t border-border/60 pt-4">
<div class="space-y-1">
<h4 class="text-sm font-semibold text-text">Active overrides</h4>
<p class="text-xs text-muted">
Videos you unblocked with “Show anyway” stay listed here until reset.
</p>
</div>
<div
id="profileModerationOverridesEmpty"
class="card border border-dashed border-border/60 p-4 text-center text-xs text-muted"
>
No moderation overrides yet.
</div>
<ul
id="profileModerationOverridesList"
class="grid-stack hidden"
data-orientation="vertical"
></ul>
</div>
</div>
</section>
+4 -1
View File
@@ -6299,7 +6299,10 @@ class Application {
const computedHidden = hideTriggered && !hideBypass;
const overrideEntry = getModerationOverride(video.id);
const overrideEntry = getModerationOverride({
eventId: video.id,
authorPubkey: video.pubkey || video.author?.pubkey || "",
});
const overrideActive = overrideEntry?.showAnyway === true;
const overrideUpdatedAt = Number.isFinite(overrideEntry?.updatedAt)
? Math.floor(overrideEntry.updatedAt)
+46 -9
View File
@@ -97,7 +97,7 @@ export default class ModerationActionController {
return false;
}
this.persistModerationOverride(video.id);
this.persistModerationOverride(video);
const target = this.resolveTargetVideo(video);
this.clearHideState(target);
@@ -199,7 +199,7 @@ export default class ModerationActionController {
this.showStatus(statusMessage, { showSpinner: false });
}
this.clearModerationOverride(video.id);
this.clearModerationOverride(video);
const target = this.resolveTargetVideo(video);
this.clearViewerOverride(target);
@@ -233,7 +233,7 @@ export default class ModerationActionController {
return false;
}
this.clearModerationOverride(video.id);
this.clearModerationOverride(video);
const target = this.resolveTargetVideo(video);
this.clearViewerOverride(target);
@@ -427,13 +427,45 @@ export default class ModerationActionController {
}
}
persistModerationOverride(eventId) {
if (!eventId || !this.services.setModerationOverride) {
getModerationOverrideDescriptor(video) {
if (!video || typeof video !== "object") {
return null;
}
const eventId = video.id;
if (!eventId) {
return null;
}
const rawAuthor =
typeof video.pubkey === "string"
? video.pubkey
: typeof video.author?.pubkey === "string"
? video.author.pubkey
: "";
const normalizedAuthor =
rawAuthor && this.auth.normalizePubkey
? this.auth.normalizePubkey(rawAuthor)
: rawAuthor;
return {
eventId,
authorPubkey: normalizedAuthor || "",
};
}
persistModerationOverride(video) {
if (!this.services.setModerationOverride) {
return;
}
const descriptor = this.getModerationOverrideDescriptor(video);
if (!descriptor) {
return;
}
try {
this.services.setModerationOverride(eventId, {
this.services.setModerationOverride(descriptor, {
showAnyway: true,
updatedAt: Date.now(),
});
@@ -445,13 +477,18 @@ export default class ModerationActionController {
}
}
clearModerationOverride(eventId) {
if (!eventId || !this.services.clearModerationOverride) {
clearModerationOverride(video) {
if (!this.services.clearModerationOverride) {
return;
}
const descriptor = this.getModerationOverrideDescriptor(video);
if (!descriptor) {
return;
}
try {
this.services.clearModerationOverride(eventId);
this.services.clearModerationOverride(descriptor);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to clear moderation override:",
+152 -31
View File
@@ -29,8 +29,9 @@ const URL_PROBE_TIMEOUT_RETRY_MS = 15 * 1000; // 15 seconds
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
const MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationOverrides:v1";
const MODERATION_OVERRIDE_STORAGE_VERSION = 1;
const MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationOverrides:v2";
const LEGACY_MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationOverrides:v1";
const MODERATION_OVERRIDE_STORAGE_VERSION = 2;
const MODERATION_SETTINGS_STORAGE_KEY = "bitvid:moderationSettings:v1";
const MODERATION_SETTINGS_STORAGE_VERSION = 3;
@@ -270,6 +271,50 @@ function normalizeEventId(value) {
return HEX64_REGEX.test(trimmed) ? trimmed : null;
}
function buildModerationOverrideKey(authorPubkey, eventId) {
const normalizedAuthor =
typeof authorPubkey === "string" ? authorPubkey.trim().toLowerCase() : "";
return `${normalizedAuthor}:${eventId}`;
}
function normalizeModerationOverrideDescriptor(eventIdOrDescriptor, authorPubkey) {
let eventId = null;
let author = null;
if (eventIdOrDescriptor && typeof eventIdOrDescriptor === "object") {
eventId =
eventIdOrDescriptor.eventId ||
eventIdOrDescriptor.id ||
eventIdOrDescriptor.eventID ||
null;
author =
eventIdOrDescriptor.authorPubkey ||
eventIdOrDescriptor.pubkey ||
eventIdOrDescriptor.author ||
null;
} else if (typeof eventIdOrDescriptor === "string") {
eventId = eventIdOrDescriptor;
}
if (!author && typeof authorPubkey === "string") {
author = authorPubkey;
}
const normalizedEventId = normalizeEventId(eventId);
if (!normalizedEventId) {
return null;
}
const normalizedAuthor = normalizeHexPubkey(author);
const authorKey = normalizedAuthor || "";
return {
eventId: normalizedEventId,
authorPubkey: normalizedAuthor || "",
key: buildModerationOverrideKey(authorKey, normalizedEventId),
};
}
export function getSavedProfiles() {
return savedProfiles;
}
@@ -1270,13 +1315,31 @@ export function getModerationOverridesMap() {
return moderationOverrides;
}
export function getModerationOverride(eventId) {
const normalized = normalizeEventId(eventId);
export function getModerationOverridesList() {
return Array.from(moderationOverrides.values()).map((entry) => ({ ...entry }));
}
function findModerationOverrideByEventId(eventId) {
for (const entry of moderationOverrides.values()) {
if (entry?.eventId === eventId) {
return entry;
}
}
return null;
}
export function getModerationOverride(eventIdOrDescriptor, authorPubkey) {
const normalized = normalizeModerationOverrideDescriptor(
eventIdOrDescriptor,
authorPubkey,
);
if (!normalized) {
return null;
}
const entry = moderationOverrides.get(normalized);
const entry =
moderationOverrides.get(normalized.key) ||
findModerationOverrideByEventId(normalized.eventId);
if (!entry) {
return null;
}
@@ -1289,39 +1352,63 @@ export function loadModerationOverridesFromStorage() {
return;
}
const raw = localStorage.getItem(MODERATION_OVERRIDE_STORAGE_KEY);
const raw =
localStorage.getItem(MODERATION_OVERRIDE_STORAGE_KEY) ||
localStorage.getItem(LEGACY_MODERATION_OVERRIDE_STORAGE_KEY);
if (!raw) {
return;
}
let shouldRewrite = false;
try {
const payload = JSON.parse(raw);
if (!payload || typeof payload !== "object") {
return;
}
if (payload.version !== MODERATION_OVERRIDE_STORAGE_VERSION) {
const entries = [];
if (payload.version === MODERATION_OVERRIDE_STORAGE_VERSION) {
if (Array.isArray(payload.entries)) {
entries.push(...payload.entries);
} else {
shouldRewrite = true;
}
} else if (payload.version === 1) {
const legacyEntries =
payload.entries && typeof payload.entries === "object"
? payload.entries
: {};
Object.entries(legacyEntries).forEach(([eventId, entry]) => {
entries.push({
eventId,
authorPubkey: "",
showAnyway: entry?.showAnyway === true,
updatedAt: entry?.updatedAt,
});
});
shouldRewrite = true;
} else {
return;
}
const entries = payload.entries && typeof payload.entries === "object"
? payload.entries
: {};
moderationOverrides.clear();
for (const [eventId, entry] of Object.entries(entries)) {
const normalized = normalizeEventId(eventId);
if (!normalized) {
for (const entry of entries) {
if (!entry || entry.showAnyway !== true) {
continue;
}
if (!entry || entry.showAnyway !== true) {
const normalized = normalizeModerationOverrideDescriptor(entry);
if (!normalized) {
continue;
}
const updatedAt = Number.isFinite(entry.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now();
moderationOverrides.set(normalized, {
moderationOverrides.set(normalized.key, {
eventId: normalized.eventId,
authorPubkey: normalized.authorPubkey,
showAnyway: true,
updatedAt,
});
@@ -1332,6 +1419,11 @@ export function loadModerationOverridesFromStorage() {
"[cache.loadModerationOverridesFromStorage] Failed to parse payload:",
error,
);
return;
}
if (shouldRewrite) {
persistModerationOverridesToStorage();
}
}
@@ -1340,23 +1432,30 @@ export function persistModerationOverridesToStorage() {
return;
}
const entries = {};
for (const [eventId, entry] of moderationOverrides.entries()) {
const entries = [];
for (const entry of moderationOverrides.values()) {
if (!entry || entry.showAnyway !== true) {
continue;
}
const normalized = normalizeModerationOverrideDescriptor(entry);
if (!normalized) {
continue;
}
const updatedAt = Number.isFinite(entry.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now();
entries[eventId] = {
entries.push({
eventId: normalized.eventId,
authorPubkey: normalized.authorPubkey,
showAnyway: true,
updatedAt,
};
});
}
if (Object.keys(entries).length === 0) {
if (entries.length === 0) {
try {
localStorage.removeItem(MODERATION_OVERRIDE_STORAGE_KEY);
localStorage.removeItem(LEGACY_MODERATION_OVERRIDE_STORAGE_KEY);
} catch (error) {
userLogger.warn(
"[cache.persistModerationOverridesToStorage] Failed to clear overrides:",
@@ -1377,6 +1476,7 @@ export function persistModerationOverridesToStorage() {
MODERATION_OVERRIDE_STORAGE_KEY,
JSON.stringify(payload),
);
localStorage.removeItem(LEGACY_MODERATION_OVERRIDE_STORAGE_KEY);
} catch (error) {
userLogger.warn(
"[cache.persistModerationOverridesToStorage] Failed to persist overrides:",
@@ -1386,18 +1486,21 @@ export function persistModerationOverridesToStorage() {
}
export function setModerationOverride(
eventId,
eventIdOrDescriptor,
override = {},
{ persist = true } = {},
) {
const normalized = normalizeEventId(eventId);
const normalized = normalizeModerationOverrideDescriptor(
eventIdOrDescriptor,
override?.authorPubkey,
);
if (!normalized) {
return null;
}
const showAnyway = override?.showAnyway === true;
if (!showAnyway) {
const removed = moderationOverrides.delete(normalized);
const removed = moderationOverrides.delete(normalized.key);
if (removed && persist) {
persistModerationOverridesToStorage();
}
@@ -1408,14 +1511,21 @@ export function setModerationOverride(
? Math.floor(override.updatedAt)
: Date.now();
const existing = moderationOverrides.get(normalized);
const nextEntry = { showAnyway: true, updatedAt };
const existing = moderationOverrides.get(normalized.key);
const nextEntry = {
eventId: normalized.eventId,
authorPubkey: normalized.authorPubkey,
showAnyway: true,
updatedAt,
};
const changed =
!existing ||
existing.showAnyway !== nextEntry.showAnyway ||
existing.updatedAt !== nextEntry.updatedAt;
existing.updatedAt !== nextEntry.updatedAt ||
existing.eventId !== nextEntry.eventId ||
existing.authorPubkey !== nextEntry.authorPubkey;
moderationOverrides.set(normalized, nextEntry);
moderationOverrides.set(normalized.key, nextEntry);
if (persist && changed) {
persistModerationOverridesToStorage();
@@ -1424,13 +1534,24 @@ export function setModerationOverride(
return { ...nextEntry };
}
export function clearModerationOverride(eventId, { persist = true } = {}) {
const normalized = normalizeEventId(eventId);
export function clearModerationOverride(
eventIdOrDescriptor,
{ persist = true } = {},
) {
const normalized = normalizeModerationOverrideDescriptor(eventIdOrDescriptor);
if (!normalized) {
return false;
}
const removed = moderationOverrides.delete(normalized);
let removed = moderationOverrides.delete(normalized.key);
for (const [key, entry] of moderationOverrides.entries()) {
if (entry?.eventId === normalized.eventId) {
moderationOverrides.delete(key);
removed = true;
}
}
if (removed && persist) {
persistModerationOverridesToStorage();
}
+5
View File
@@ -37,9 +37,11 @@ import { isWatchHistoryDebugEnabled } from "../watchHistoryDebug.js";
import { splitAndZap as splitAndZapDefault } from "../payments/zapSplit.js";
import {
getDefaultModerationSettings,
getModerationOverridesList,
getModerationSettings,
setModerationSettings,
resetModerationSettings,
clearModerationOverride,
persistSavedProfiles,
getSavedProfiles,
getActiveProfilePubkey,
@@ -443,6 +445,9 @@ export default class ApplicationBootstrap {
setModerationSettings(partial),
resetModerationSettings: () => resetModerationSettings(),
},
getModerationOverrides: () => getModerationOverridesList(),
clearModerationOverride: (descriptor) =>
clearModerationOverride(descriptor),
loadVideos: (forceFetch, context) =>
app.loadVideos(forceFetch, context),
onVideosShouldRefresh: (context) =>
+241
View File
@@ -503,6 +503,20 @@ const SERVICE_CONTRACT = [
return service;
},
},
{
key: "getModerationOverrides",
type: "function",
description:
"Returns a list of stored moderation overrides for the active viewer.",
fallback: () => () => [],
},
{
key: "clearModerationOverride",
type: "function",
description:
"Clears a specific moderation override when the viewer resets a decision.",
fallback: () => () => false,
},
{
key: "loadVideos",
type: "function",
@@ -1099,8 +1113,11 @@ export class ProfileModalController {
this.moderationSaveButton = null;
this.moderationResetButton = null;
this.moderationStatusText = null;
this.moderationOverridesList = null;
this.moderationOverridesEmpty = null;
this.moderationHideControlsGroup = null;
this.moderationHideControlElements = [];
this.boundModerationOverridesUpdate = null;
this.moderatorSection = null;
this.moderatorEmpty = null;
this.adminModeratorList = null;
@@ -1415,6 +1432,10 @@ export class ProfileModalController {
document.getElementById("profileModerationReset") || null;
this.moderationStatusText =
document.getElementById("profileModerationStatus") || null;
this.moderationOverridesList =
document.getElementById("profileModerationOverridesList") || null;
this.moderationOverridesEmpty =
document.getElementById("profileModerationOverridesEmpty") || null;
this.moderationTrustedContactsCount =
document.getElementById("profileModerationTrustedContactsCount") || null;
this.moderationTrustedMuteCount =
@@ -2688,6 +2709,24 @@ export class ProfileModalController {
});
}
if (!this.boundModerationOverridesUpdate && typeof document !== "undefined") {
this.boundModerationOverridesUpdate = () => {
this.refreshModerationOverridesUi();
};
document.addEventListener(
"video:moderation-override",
this.boundModerationOverridesUpdate,
);
document.addEventListener(
"video:moderation-hide",
this.boundModerationOverridesUpdate,
);
document.addEventListener(
"video:moderation-block",
this.boundModerationOverridesUpdate,
);
}
if (this.addModeratorButton instanceof HTMLElement) {
this.addModeratorButton.addEventListener("click", () => {
void this.handleAddModerator();
@@ -7084,6 +7123,206 @@ export class ProfileModalController {
helper.textContent = TRUSTED_MUTE_HIDE_HELPER_TEXT;
}
getModerationOverrideEntries() {
if (typeof this.services.getModerationOverrides !== "function") {
return [];
}
try {
const entries = this.services.getModerationOverrides();
return Array.isArray(entries) ? entries : [];
} catch (error) {
devLogger.info(
"[profileModal] moderation overrides fallback used",
error,
);
return [];
}
}
normalizeModerationOverrideEntries(entries = []) {
const normalized = [];
const seen = new Set();
entries.forEach((entry) => {
if (!entry || typeof entry !== "object") {
return;
}
const eventId =
typeof entry.eventId === "string"
? entry.eventId.trim().toLowerCase()
: "";
if (!eventId) {
return;
}
const author =
typeof entry.authorPubkey === "string"
? entry.authorPubkey.trim()
: "";
const normalizedAuthor = author ? this.normalizeHexPubkey(author) || author : "";
const key = `${normalizedAuthor || ""}:${eventId}`;
if (seen.has(key)) {
return;
}
seen.add(key);
normalized.push({
eventId,
authorPubkey: normalizedAuthor || "",
updatedAt: Number.isFinite(entry.updatedAt)
? Math.floor(entry.updatedAt)
: 0,
});
});
normalized.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
return normalized;
}
formatModerationOverrideTimestamp(updatedAt) {
const numeric = Number(updatedAt);
if (!Number.isFinite(numeric) || numeric <= 0) {
return { display: "", iso: "" };
}
try {
const date = new Date(numeric);
return {
display: date.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
}),
iso: date.toISOString(),
};
} catch (error) {
return { display: "", iso: "" };
}
}
async handleModerationOverrideReset(entry) {
if (!entry || typeof entry !== "object") {
return false;
}
if (typeof this.services.clearModerationOverride !== "function") {
return false;
}
try {
await this.services.clearModerationOverride({
eventId: entry.eventId,
authorPubkey: entry.authorPubkey,
});
this.refreshModerationOverridesUi();
this.showSuccess("Moderation override reset.");
return true;
} catch (error) {
this.showError("Unable to reset this moderation override.");
return false;
}
}
refreshModerationOverridesUi() {
if (
!(this.moderationOverridesList instanceof HTMLElement) ||
!(this.moderationOverridesEmpty instanceof HTMLElement)
) {
return;
}
const entries = this.normalizeModerationOverrideEntries(
this.getModerationOverrideEntries(),
);
this.moderationOverridesList.innerHTML = "";
if (!entries.length) {
this.moderationOverridesEmpty.classList.remove("hidden");
this.moderationOverridesList.classList.add("hidden");
return;
}
this.moderationOverridesEmpty.classList.add("hidden");
this.moderationOverridesList.classList.remove("hidden");
const entriesNeedingFetch = new Set();
entries.forEach((entry) => {
const item = document.createElement("li");
item.className = "card space-y-2 p-4";
const row = document.createElement("div");
row.className = "flex items-center justify-between gap-4";
const authorKey = entry.authorPubkey;
let profileSummary = null;
if (authorKey) {
const cacheEntry = this.services.getProfileCacheEntry(authorKey);
if (!cacheEntry) {
entriesNeedingFetch.add(authorKey);
}
}
const summaryData = this.resolveProfileSummaryForPubkey(authorKey);
profileSummary = this.createCompactProfileSummary(summaryData);
const actions = document.createElement("div");
actions.className = "flex flex-wrap items-center justify-end gap-2";
const resetButton = this.createRemoveButton({
label: "Reset",
onRemove: () => this.handleModerationOverrideReset(entry),
});
if (resetButton) {
actions.appendChild(resetButton);
}
if (profileSummary) {
row.appendChild(profileSummary);
}
if (actions.childElementCount > 0) {
row.appendChild(actions);
}
const meta = document.createElement("div");
meta.className = "flex flex-wrap items-center gap-3 text-2xs text-muted";
const contentId = document.createElement("span");
contentId.className = "font-mono text-2xs text-muted";
const shortId =
typeof this.truncateMiddle === "function"
? this.truncateMiddle(entry.eventId, 16)
: entry.eventId;
contentId.textContent = `Content ${shortId}`;
contentId.title = entry.eventId;
meta.appendChild(contentId);
const timestamp = this.formatModerationOverrideTimestamp(entry.updatedAt);
if (timestamp.display) {
const time = document.createElement("time");
time.className = "text-2xs text-muted";
time.dateTime = timestamp.iso;
time.textContent = `Updated ${timestamp.display}`;
meta.appendChild(time);
}
item.appendChild(row);
item.appendChild(meta);
this.moderationOverridesList.appendChild(item);
});
if (
entriesNeedingFetch.size &&
typeof this.services.batchFetchProfiles === "function"
) {
this.services.batchFetchProfiles(entriesNeedingFetch);
}
}
refreshModerationSettingsUi() {
const service = this.getModerationSettingsService();
if (!service) {
@@ -7091,6 +7330,7 @@ export class ProfileModalController {
this.currentModerationSettings = createInternalDefaultModerationSettings();
this.updateTrustedHideControlsVisibility();
this.updateModerationTrustStats();
this.refreshModerationOverridesUi();
this.applyModerationSettingsControlState({ resetStatus: true });
return;
}
@@ -7133,6 +7373,7 @@ export class ProfileModalController {
this.updateTrustedHideControlsVisibility();
this.updateModerationTrustStats();
this.refreshModerationOverridesUi();
this.applyModerationSettingsControlState({ resetStatus: true });
}