mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 10:22:25 +00:00
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:
6 files changed
+467
-41
No files matched your search
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user