import { DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, DEFAULT_BLUR_THRESHOLD, DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, getTrustedMuteHideThreshold, getTrustedSpamHideThreshold, PROFILE_CACHE_TTL_MS, URL_HEALTH_TTL_MS, URL_HEALTH_RETRY_MS, URL_PROBE_TIMEOUT_MS, } from "../constants.js"; import { userLogger } from "../utils/logger.js"; import { sanitizeProfileMediaUrl } from "../utils/profileMedia.js"; import { readUrlHealthFromStorage, removeUrlHealthFromStorage, writeUrlHealthToStorage, } from "../utils/storage.js"; import createProfileSettingsStore from "./profileSettingsStore.js"; const PROFILE_CACHE_STORAGE_KEY = "bitvid:profileCache:v1"; const PROFILE_CACHE_VERSION = 1; const SAVED_PROFILES_STORAGE_KEY = "bitvid:savedProfiles:v1"; const SAVED_PROFILES_STORAGE_VERSION = 1; const URL_HEALTH_TIMEOUT_RETRY_MS = URL_HEALTH_RETRY_MS; export { URL_PROBE_TIMEOUT_MS }; const URL_PROBE_TIMEOUT_RETRY_MS = 15 * 1000; // 15 seconds const MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationOverrides:v2"; const MODERATION_OVERRIDE_STORAGE_VERSION = 2; // Account-level ("trusted creator") web-of-trust overrides, keyed by author hex. // A showAnyway entry suppresses the WoT blur/hide/warning for ALL of that // author's videos until the viewer resets it in the Safety tab. const AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationAuthorOverrides:v1"; const AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION = 1; const MODERATION_SETTINGS_STORAGE_KEY = "bitvid:moderationSettings:v1"; const MODERATION_SETTINGS_STORAGE_VERSION = 3; const DM_PRIVACY_SETTINGS_STORAGE_KEY = "bitvid:dmPrivacySettings:v1"; const DM_PRIVACY_SETTINGS_STORAGE_VERSION = 1; function computeDefaultModerationSettings() { const defaultBlur = sanitizeModerationThreshold( DEFAULT_BLUR_THRESHOLD, DEFAULT_BLUR_THRESHOLD, ); const defaultAutoplay = sanitizeModerationThreshold( DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, ); const defaultTrustedMute = sanitizeModerationThreshold( DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, ); const defaultTrustedSpam = sanitizeModerationThreshold( DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, ); let runtimeMuteHide = defaultTrustedMute; let runtimeSpamHide = defaultTrustedSpam; try { if (typeof getTrustedMuteHideThreshold === "function") { runtimeMuteHide = sanitizeModerationThreshold( getTrustedMuteHideThreshold(), defaultTrustedMute, ); } } catch (error) { userLogger.warn( "[cache.computeDefaultModerationSettings] Failed to read runtime mute hide threshold:", error, ); runtimeMuteHide = defaultTrustedMute; } try { if (typeof getTrustedSpamHideThreshold === "function") { runtimeSpamHide = sanitizeModerationThreshold( getTrustedSpamHideThreshold(), defaultTrustedSpam, ); } } catch (error) { userLogger.warn( "[cache.computeDefaultModerationSettings] Failed to read runtime spam hide threshold:", error, ); runtimeSpamHide = defaultTrustedSpam; } return { blurThreshold: defaultBlur, autoplayBlockThreshold: defaultAutoplay, trustedMuteHideThreshold: sanitizeModerationThreshold( runtimeMuteHide, defaultTrustedMute, ), trustedMuteHideThresholds: {}, trustedSpamHideThreshold: sanitizeModerationThreshold( runtimeSpamHide, defaultTrustedSpam, ), }; } function createDefaultModerationSettings() { return { ...computeDefaultModerationSettings() }; } const GLOBAL_MODERATION_PROFILE_KEY = "__global__"; const moderationSettingsStore = createProfileSettingsStore({ clone: (value) => ({ ...value }), }); moderationSettingsStore.set( GLOBAL_MODERATION_PROFILE_KEY, createDefaultModerationSettings(), ); function createDefaultDmPrivacySettings() { return { readReceiptsEnabled: false, typingIndicatorsEnabled: false, }; } const GLOBAL_DM_PRIVACY_PROFILE_KEY = "__dmPrivacyDefault__"; const dmPrivacySettingsStore = createProfileSettingsStore({ clone: (value) => ({ ...value }), }); dmPrivacySettingsStore.set( GLOBAL_DM_PRIVACY_PROFILE_KEY, createDefaultDmPrivacySettings(), ); function sanitizeModerationThreshold(value, fallback) { if (value === null || value === undefined) { return fallback; } const numeric = Number(value); if (!Number.isFinite(numeric)) { return fallback; } const clamped = Math.max(0, Math.floor(numeric)); if (!Number.isFinite(clamped)) { return fallback; } return clamped; } function sanitizeModerationThresholdMap( value, fallback = {}, { mergeFallback = false } = {}, ) { const fallbackMap = fallback && typeof fallback === "object" ? fallback : {}; if (!value || typeof value !== "object") { return mergeFallback ? { ...fallbackMap } : {}; } const sanitized = {}; for (const [key, entry] of Object.entries(value)) { if (typeof key !== "string") { continue; } const normalizedKey = key.trim().toLowerCase(); if (!normalizedKey) { continue; } const sanitizedValue = sanitizeModerationThreshold( entry, fallbackMap[normalizedKey], ); if (Number.isFinite(sanitizedValue)) { sanitized[normalizedKey] = sanitizedValue; } } return mergeFallback ? { ...fallbackMap, ...sanitized } : sanitized; } function sanitizeProfileString(value) { if (typeof value !== "string") { return ""; } const trimmed = value.trim(); return trimmed; } let savedProfiles = []; let activeProfilePubkey = null; let hadExplicitActiveProfile = false; const profileCache = new Map(); const urlHealthCache = new Map(); const urlHealthInFlight = new Map(); const moderationOverrides = new Map(); const authorModerationOverrides = new Map(); function hasSavedProfilesChanged(previousProfiles, nextProfiles) { if (previousProfiles === nextProfiles) { return false; } if (!Array.isArray(previousProfiles) || !Array.isArray(nextProfiles)) { return true; } if (previousProfiles.length !== nextProfiles.length) { return true; } for (let index = 0; index < previousProfiles.length; index += 1) { const prev = previousProfiles[index]; const next = nextProfiles[index]; if (prev === next) { continue; } if (!prev || !next || typeof prev !== "object" || typeof next !== "object") { return true; } const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); for (const key of keys) { if (prev[key] !== next[key]) { return true; } } } return false; } function safeDecodeNpub(npub) { if (typeof npub !== "string") { return null; } const trimmed = npub.trim(); if (!trimmed) { return null; } try { const decoded = window?.NostrTools?.nip19?.decode(trimmed); if (decoded && decoded.type === "npub" && typeof decoded.data === "string") { return decoded.data; } } catch (error) { return null; } return null; } function normalizeHexPubkey(pubkey) { if (typeof pubkey !== "string") { return null; } const trimmed = pubkey.trim(); if (!trimmed) { return null; } if (HEX64_REGEX.test(trimmed)) { return trimmed.toLowerCase(); } if (trimmed.startsWith("npub1")) { const decoded = safeDecodeNpub(trimmed); if (decoded && HEX64_REGEX.test(decoded)) { return decoded.toLowerCase(); } } return null; } function normalizeEventId(value) { if (typeof value !== "string") { return null; } const trimmed = value.trim().toLowerCase(); if (!trimmed) { return null; } 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; } export function getProfileCacheMap() { return profileCache; } export function getActiveProfilePubkey() { return activeProfilePubkey; } export function setActiveProfilePubkey(pubkey, { persist = true } = {}) { const normalized = normalizeHexPubkey(pubkey); const nextValue = normalized || (typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : null); if (activeProfilePubkey === nextValue) { if (persist) { persistSavedProfiles({ persistActive: true }); } return activeProfilePubkey; } activeProfilePubkey = nextValue; ensureModerationSettingsForKey(getActiveModerationStoreKey()); if (persist) { persistSavedProfiles({ persistActive: true }); } return activeProfilePubkey; } export function mutateSavedProfiles(mutator, { persist = true, persistActive = true } = {}) { if (typeof mutator !== "function") { return { changed: false, profiles: getSavedProfiles() }; } const draft = savedProfiles.slice(); const result = mutator(draft); const nextProfiles = Array.isArray(result) ? result : draft; const changed = hasSavedProfilesChanged(savedProfiles, nextProfiles); if (changed) { savedProfiles = nextProfiles; if (persist) { persistSavedProfiles({ persistActive }); } } else if (persist && persistActive === false) { persistSavedProfiles({ persistActive: false }); } return { changed, profiles: getSavedProfiles() }; } export function setSavedProfiles(nextProfiles, options) { return mutateSavedProfiles( () => (Array.isArray(nextProfiles) ? nextProfiles.slice() : []), options ); } export function readSavedProfilesPayloadFromStorage() { if (typeof localStorage === "undefined") { return null; } const raw = localStorage.getItem(SAVED_PROFILES_STORAGE_KEY); if (!raw) { return null; } try { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") { return null; } return parsed; } catch (error) { userLogger.warn( "[cache.readSavedProfilesPayloadFromStorage] Failed to parse payload:", error ); return null; } } function writeSavedProfilesPayloadToStorage(payload) { if (typeof localStorage === "undefined") { return; } try { localStorage.setItem(SAVED_PROFILES_STORAGE_KEY, JSON.stringify(payload)); } catch (error) { const isQuotaError = error && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED" || error.code === 22 || error.code === 1014); if (isQuotaError) { userLogger.warn( "[cache.writeSavedProfilesPayloadToStorage] Storage quota exceeded; keeping in-memory copy only.", error ); } else { userLogger.warn( "[cache.writeSavedProfilesPayloadToStorage] Failed to persist saved profiles:", error ); } } } export function persistSavedProfiles({ persistActive = true } = {}) { if (typeof localStorage === "undefined") { return; } if (!savedProfiles.length && !activeProfilePubkey) { try { localStorage.removeItem(SAVED_PROFILES_STORAGE_KEY); } catch (error) { userLogger.warn( "[cache.persistSavedProfiles] Failed to remove empty payload:", error ); } return; } let activePubkeyToPersist = activeProfilePubkey || null; if (!persistActive) { const storedPayload = readSavedProfilesPayloadFromStorage(); if (storedPayload && typeof storedPayload === "object") { const candidate = typeof storedPayload.activePubkey === "string" ? storedPayload.activePubkey : typeof storedPayload.activePubKey === "string" ? storedPayload.activePubKey : null; const normalizedStored = normalizeHexPubkey(candidate); if (normalizedStored) { activePubkeyToPersist = normalizedStored; } else if (candidate === null) { activePubkeyToPersist = null; } } } const payload = { version: SAVED_PROFILES_STORAGE_VERSION, entries: savedProfiles.map((entry) => ({ pubkey: entry.pubkey, npub: typeof entry.npub === "string" && entry.npub.trim() ? entry.npub.trim() : null, name: typeof entry.name === "string" ? entry.name : "", picture: typeof entry.picture === "string" ? entry.picture : "", authType: typeof entry.authType === "string" && entry.authType.trim() ? entry.authType.trim() : null, providerId: typeof entry.providerId === "string" && entry.providerId.trim() ? entry.providerId.trim() : null, })), activePubkey: activePubkeyToPersist, }; writeSavedProfilesPayloadToStorage(payload); } function processSavedProfilesPayload(parsed) { const result = { profiles: [], activePubkey: null, hasExplicitActiveProfile: false, needsRewrite: false, }; if ( !parsed || typeof parsed !== "object" || parsed.version !== SAVED_PROFILES_STORAGE_VERSION ) { result.needsRewrite = true; return result; } const entries = Array.isArray(parsed.entries) ? parsed.entries : []; const seenPubkeys = new Set(); for (const candidate of entries) { if (!candidate || typeof candidate !== "object") { continue; } const normalizedPubkey = normalizeHexPubkey(candidate.pubkey); if (!normalizedPubkey || seenPubkeys.has(normalizedPubkey)) { result.needsRewrite = true; continue; } seenPubkeys.add(normalizedPubkey); const rawAuthType = typeof candidate.authType === "string" ? candidate.authType : null; let storedAuthType = null; if (rawAuthType) { const trimmedAuthType = rawAuthType.trim(); if (trimmedAuthType) { storedAuthType = trimmedAuthType; if (trimmedAuthType !== rawAuthType) { result.needsRewrite = true; } } } if (!storedAuthType) { storedAuthType = "nip07"; if (rawAuthType !== "nip07") { result.needsRewrite = true; } } const rawProviderId = typeof candidate.providerId === "string" ? candidate.providerId : null; let storedProviderId = null; if (rawProviderId) { const trimmedProviderId = rawProviderId.trim(); if (trimmedProviderId) { storedProviderId = trimmedProviderId; if (trimmedProviderId !== rawProviderId) { result.needsRewrite = true; } } } if (!storedProviderId && storedAuthType) { storedProviderId = storedAuthType; if (rawProviderId !== storedProviderId) { result.needsRewrite = true; } } const npub = typeof candidate.npub === "string" && candidate.npub.trim() ? candidate.npub.trim() : null; const entry = { pubkey: normalizedPubkey, npub, name: typeof candidate.name === "string" ? candidate.name : "", picture: typeof candidate.picture === "string" ? candidate.picture : "", authType: storedAuthType, providerId: storedProviderId, }; if ( typeof candidate.npub === "string" && candidate.npub.trim() !== candidate.npub ) { result.needsRewrite = true; } if ( entry.npub && typeof candidate.npub !== "string" && entry.npub !== candidate.npub ) { result.needsRewrite = true; } result.profiles.push(entry); } let hasActiveField = false; let activeCandidate; if (Object.prototype.hasOwnProperty.call(parsed, "activePubkey")) { hasActiveField = true; activeCandidate = parsed.activePubkey; } else if (Object.prototype.hasOwnProperty.call(parsed, "activePubKey")) { hasActiveField = true; activeCandidate = parsed.activePubKey; } if (hasActiveField) { result.hasExplicitActiveProfile = true; if (typeof activeCandidate === "string") { const normalizedActive = normalizeHexPubkey(activeCandidate); if (normalizedActive && seenPubkeys.has(normalizedActive)) { result.activePubkey = normalizedActive; } else if (activeCandidate) { result.needsRewrite = true; } } else if (activeCandidate !== null && activeCandidate !== undefined) { result.needsRewrite = true; } } return result; } export function loadSavedProfilesFromStorage() { savedProfiles = []; activeProfilePubkey = null; hadExplicitActiveProfile = false; if (typeof localStorage === "undefined") { return { profiles: getSavedProfiles(), activePubkey: activeProfilePubkey, hasExplicitActiveProfile: hadExplicitActiveProfile, }; } const raw = localStorage.getItem(SAVED_PROFILES_STORAGE_KEY); if (!raw) { return { profiles: getSavedProfiles(), activePubkey: activeProfilePubkey, hasExplicitActiveProfile: hadExplicitActiveProfile, }; } let needsRewrite = false; try { const parsed = JSON.parse(raw); const result = processSavedProfilesPayload(parsed); savedProfiles = result.profiles; activeProfilePubkey = result.activePubkey; hadExplicitActiveProfile = result.hasExplicitActiveProfile; needsRewrite = result.needsRewrite; } catch (error) { userLogger.warn( "[cache.loadSavedProfilesFromStorage] Failed to parse payload:", error, ); needsRewrite = true; } if ( !activeProfilePubkey && savedProfiles.length && !hadExplicitActiveProfile ) { activeProfilePubkey = savedProfiles[0].pubkey; needsRewrite = true; } if (needsRewrite) { persistSavedProfiles(); } return { profiles: getSavedProfiles(), activePubkey: activeProfilePubkey, hasExplicitActiveProfile: hadExplicitActiveProfile, }; } export function syncSavedProfileFromCache(pubkey, { persist = false } = {}) { const normalized = normalizeHexPubkey(pubkey); if (!normalized) { return false; } const cacheEntry = profileCache.get(normalized); if (!cacheEntry || typeof cacheEntry !== "object") { return false; } let updated = false; mutateSavedProfiles((profiles) => { const next = profiles.slice(); const index = next.findIndex((entry) => entry && entry.pubkey === normalized); if (index < 0) { return profiles; } const existing = next[index] || {}; const profile = cacheEntry.profile || {}; const nextEntry = { ...existing, name: profile.name || existing.name || "", picture: profile.picture || existing.picture || "", }; if ( existing.name === nextEntry.name && existing.picture === nextEntry.picture ) { return profiles; } next[index] = nextEntry; updated = true; return next; }, { persist, persistActive: false }); return updated; } export function loadProfileCacheFromStorage() { if (typeof localStorage === "undefined") { return; } const now = Date.now(); const raw = localStorage.getItem(PROFILE_CACHE_STORAGE_KEY); if (!raw) { return; } try { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") { return; } if (parsed.version !== PROFILE_CACHE_VERSION) { return; } const entries = parsed.entries; if (!entries || typeof entries !== "object") { return; } for (const [pubkey, entry] of Object.entries(entries)) { if (!pubkey || !entry || typeof entry !== "object") { continue; } const timestamp = typeof entry.timestamp === "number" ? entry.timestamp : 0; if (!timestamp || now - timestamp > PROFILE_CACHE_TTL_MS) { continue; } const profile = entry.profile; if (!profile || typeof profile !== "object") { continue; } const normalized = { name: profile.name || profile.display_name || "Unknown", picture: profile.picture || "assets/svg/default-profile.svg", }; const about = sanitizeProfileString(profile.about || profile.aboutMe); if (about) { normalized.about = about; } const website = sanitizeProfileString(profile.website || profile.url); if (website) { normalized.website = website; } const banner = sanitizeProfileString(profile.banner || profile.header); if (banner) { normalized.banner = banner; } const lud16 = sanitizeProfileString(profile.lud16); if (lud16) { normalized.lud16 = lud16; } const lud06 = sanitizeProfileString(profile.lud06); if (lud06) { normalized.lud06 = lud06; } const lightningCandidates = [ sanitizeProfileString(profile.lightningAddress), lud16, lud06, ].filter(Boolean); if (lightningCandidates.length) { normalized.lightningAddress = lightningCandidates[0]; } profileCache.set(pubkey, { profile: normalized, timestamp, }); } } catch (error) { userLogger.warn("[cache.loadProfileCacheFromStorage] Failed to parse payload:", error); } } export function persistProfileCacheToStorage() { if (typeof localStorage === "undefined") { return; } const now = Date.now(); const entries = {}; for (const [pubkey, entry] of profileCache.entries()) { if (!entry || typeof entry !== "object") { continue; } const timestamp = typeof entry.timestamp === "number" ? entry.timestamp : 0; if (!timestamp || now - timestamp > PROFILE_CACHE_TTL_MS) { profileCache.delete(pubkey); continue; } entries[pubkey] = { profile: entry.profile, timestamp, }; } const payload = { version: PROFILE_CACHE_VERSION, savedAt: now, entries, }; if (Object.keys(entries).length === 0) { try { localStorage.removeItem(PROFILE_CACHE_STORAGE_KEY); } catch (error) { userLogger.warn( "[cache.persistProfileCacheToStorage] Failed to clear storage:", error ); } return; } try { localStorage.setItem(PROFILE_CACHE_STORAGE_KEY, JSON.stringify(payload)); } catch (error) { userLogger.warn("[cache.persistProfileCacheToStorage] Failed to persist cache:", error); } } import { profileCache as unifiedProfileCache } from "./profileCache.js"; import { HEX64_REGEX } from "../utils/hex.js"; export function getProfileCacheEntry(pubkey) { const profile = unifiedProfileCache.getProfile(pubkey); // Maintain backward compatibility with { profile, timestamp } return shape // although consumers mostly check .profile if (profile) { // We can fetch the raw data to get the timestamp if needed, but for now // returning a synthetic entry matches most usage patterns. return { profile, timestamp: Date.now() }; } return null; } export function setProfileCacheEntry(pubkey, profile, { persist = true } = {}) { const entry = unifiedProfileCache.setProfile(pubkey, profile, { persist }); return entry; } function haveModerationSettingsChanged(previous, next) { if (!previous || !next) { return true; } const prevMuteThresholds = previous.trustedMuteHideThresholds && typeof previous.trustedMuteHideThresholds === "object" ? previous.trustedMuteHideThresholds : {}; const nextMuteThresholds = next.trustedMuteHideThresholds && typeof next.trustedMuteHideThresholds === "object" ? next.trustedMuteHideThresholds : {}; const allMuteKeys = new Set([ ...Object.keys(prevMuteThresholds), ...Object.keys(nextMuteThresholds), ]); for (const key of allMuteKeys) { if (prevMuteThresholds[key] !== nextMuteThresholds[key]) { return true; } } return ( previous.blurThreshold !== next.blurThreshold || previous.autoplayBlockThreshold !== next.autoplayBlockThreshold || previous.trustedMuteHideThreshold !== next.trustedMuteHideThreshold || previous.trustedSpamHideThreshold !== next.trustedSpamHideThreshold ); } function getModerationStoreKeyForPubkey(pubkey) { const normalized = normalizeHexPubkey(pubkey); return normalized || GLOBAL_MODERATION_PROFILE_KEY; } function getActiveModerationStoreKey() { return getModerationStoreKeyForPubkey(activeProfilePubkey); } function ensureModerationSettingsForKey(key) { const storeKey = key || GLOBAL_MODERATION_PROFILE_KEY; if (!moderationSettingsStore.has(storeKey)) { moderationSettingsStore.set(storeKey, createDefaultModerationSettings()); } return moderationSettingsStore.get(storeKey); } export function getDefaultModerationSettings() { return createDefaultModerationSettings(); } export function getModerationSettingsForPubkey(pubkey) { const key = getModerationStoreKeyForPubkey(pubkey); const settings = ensureModerationSettingsForKey(key); return { ...settings }; } export function getModerationSettings() { return getModerationSettingsForPubkey(activeProfilePubkey); } export function loadModerationSettingsFromStorage() { moderationSettingsStore.clear(); moderationSettingsStore.set( GLOBAL_MODERATION_PROFILE_KEY, createDefaultModerationSettings(), ); if (typeof localStorage === "undefined") { return getModerationSettings(); } const raw = localStorage.getItem(MODERATION_SETTINGS_STORAGE_KEY); if (!raw) { return getModerationSettings(); } let shouldRewrite = false; try { const payload = JSON.parse(raw); if (!payload || typeof payload !== "object") { shouldRewrite = true; } else if (payload.version === 1) { const defaults = createDefaultModerationSettings(); const overrides = payload.overrides && typeof payload.overrides === "object" ? payload.overrides : {}; const legacySettings = { blurThreshold: sanitizeModerationThreshold( overrides.blurThreshold, defaults.blurThreshold, ), autoplayBlockThreshold: sanitizeModerationThreshold( overrides.autoplayBlockThreshold, defaults.autoplayBlockThreshold, ), trustedMuteHideThreshold: sanitizeModerationThreshold( overrides.trustedMuteHideThreshold, defaults.trustedMuteHideThreshold, ), trustedMuteHideThresholds: sanitizeModerationThresholdMap( overrides.trustedMuteHideThresholds, defaults.trustedMuteHideThresholds, { mergeFallback: true }, ), trustedSpamHideThreshold: sanitizeModerationThreshold( overrides.trustedSpamHideThreshold, defaults.trustedSpamHideThreshold, ), }; moderationSettingsStore.set( GLOBAL_MODERATION_PROFILE_KEY, legacySettings, ); shouldRewrite = true; } else if (payload.version === 2 || payload.version === MODERATION_SETTINGS_STORAGE_VERSION) { const defaults = createDefaultModerationSettings(); const entries = payload.entries && typeof payload.entries === "object" ? payload.entries : {}; for (const [key, entry] of Object.entries(entries)) { if (!entry || typeof entry !== "object") { shouldRewrite = true; continue; } const overrides = entry.overrides && typeof entry.overrides === "object" ? entry.overrides : {}; const storeKey = key === "default" ? GLOBAL_MODERATION_PROFILE_KEY : getModerationStoreKeyForPubkey(key); if (!storeKey) { shouldRewrite = true; continue; } const settings = { blurThreshold: sanitizeModerationThreshold( overrides.blurThreshold, defaults.blurThreshold, ), autoplayBlockThreshold: sanitizeModerationThreshold( overrides.autoplayBlockThreshold, defaults.autoplayBlockThreshold, ), trustedMuteHideThreshold: sanitizeModerationThreshold( overrides.trustedMuteHideThreshold, defaults.trustedMuteHideThreshold, ), trustedMuteHideThresholds: sanitizeModerationThresholdMap( overrides.trustedMuteHideThresholds, defaults.trustedMuteHideThresholds, { mergeFallback: true }, ), trustedSpamHideThreshold: sanitizeModerationThreshold( overrides.trustedSpamHideThreshold, defaults.trustedSpamHideThreshold, ), }; moderationSettingsStore.set(storeKey, settings); } } else { shouldRewrite = true; } } catch (error) { userLogger.warn( "[cache.loadModerationSettingsFromStorage] Failed to parse payload:", error, ); shouldRewrite = true; } if (shouldRewrite) { persistModerationSettingsToStorage(); } return getModerationSettings(); } function persistModerationSettingsToStorage() { if (typeof localStorage === "undefined") { return; } const defaults = createDefaultModerationSettings(); const entries = {}; for (const [key, value] of moderationSettingsStore.entries()) { const settings = value && typeof value === "object" ? value : createDefaultModerationSettings(); const overrides = {}; if (settings.blurThreshold !== defaults.blurThreshold) { overrides.blurThreshold = settings.blurThreshold; } if (settings.autoplayBlockThreshold !== defaults.autoplayBlockThreshold) { overrides.autoplayBlockThreshold = settings.autoplayBlockThreshold; } if (settings.trustedMuteHideThreshold !== defaults.trustedMuteHideThreshold) { overrides.trustedMuteHideThreshold = settings.trustedMuteHideThreshold; } const trustedMuteHideThresholds = sanitizeModerationThresholdMap( settings.trustedMuteHideThresholds, defaults.trustedMuteHideThresholds, { mergeFallback: false }, ); if (Object.keys(trustedMuteHideThresholds).length > 0) { overrides.trustedMuteHideThresholds = trustedMuteHideThresholds; } if (settings.trustedSpamHideThreshold !== defaults.trustedSpamHideThreshold) { overrides.trustedSpamHideThreshold = settings.trustedSpamHideThreshold; } if (Object.keys(overrides).length === 0) { continue; } const storageKey = key === GLOBAL_MODERATION_PROFILE_KEY ? "default" : key; entries[storageKey] = { overrides, savedAt: Date.now(), }; } if (Object.keys(entries).length === 0) { try { localStorage.removeItem(MODERATION_SETTINGS_STORAGE_KEY); } catch (error) { userLogger.warn( "[cache.persistModerationSettingsToStorage] Failed to clear overrides:", error, ); } return; } const payload = { version: MODERATION_SETTINGS_STORAGE_VERSION, entries, savedAt: Date.now(), }; try { localStorage.setItem( MODERATION_SETTINGS_STORAGE_KEY, JSON.stringify(payload), ); } catch (error) { userLogger.warn( "[cache.persistModerationSettingsToStorage] Failed to persist overrides:", error, ); } } function getDmPrivacyStoreKeyForPubkey(pubkey) { const normalized = normalizeHexPubkey(pubkey); return normalized || GLOBAL_DM_PRIVACY_PROFILE_KEY; } function ensureDmPrivacySettingsForKey(key) { const storeKey = key || GLOBAL_DM_PRIVACY_PROFILE_KEY; if (!dmPrivacySettingsStore.has(storeKey)) { dmPrivacySettingsStore.set(storeKey, createDefaultDmPrivacySettings()); } return dmPrivacySettingsStore.get(storeKey); } function normalizeDmPrivacySettings(partial = {}) { const defaults = createDefaultDmPrivacySettings(); const resolved = partial && typeof partial === "object" ? partial : {}; return { readReceiptsEnabled: typeof resolved.readReceiptsEnabled === "boolean" ? resolved.readReceiptsEnabled : defaults.readReceiptsEnabled, typingIndicatorsEnabled: typeof resolved.typingIndicatorsEnabled === "boolean" ? resolved.typingIndicatorsEnabled : defaults.typingIndicatorsEnabled, }; } export function getDmPrivacySettingsForPubkey(pubkey) { const key = getDmPrivacyStoreKeyForPubkey(pubkey); const settings = ensureDmPrivacySettingsForKey(key); return { ...settings }; } export function getDmPrivacySettings() { return getDmPrivacySettingsForPubkey(activeProfilePubkey); } export function setDmPrivacySettings(partial = {}, options = {}) { const targetPubkey = typeof options?.pubkey === "string" ? options.pubkey : activeProfilePubkey; const key = getDmPrivacyStoreKeyForPubkey(targetPubkey); const existing = ensureDmPrivacySettingsForKey(key); const normalized = normalizeDmPrivacySettings({ ...existing, ...partial }); dmPrivacySettingsStore.set(key, normalized); persistDmPrivacySettingsToStorage(); return { ...normalized }; } export function loadDmPrivacySettingsFromStorage() { dmPrivacySettingsStore.clear(); dmPrivacySettingsStore.set( GLOBAL_DM_PRIVACY_PROFILE_KEY, createDefaultDmPrivacySettings(), ); if (typeof localStorage === "undefined") { return getDmPrivacySettings(); } const raw = localStorage.getItem(DM_PRIVACY_SETTINGS_STORAGE_KEY); if (!raw) { return getDmPrivacySettings(); } let shouldRewrite = false; try { const payload = JSON.parse(raw); if (!payload || typeof payload !== "object") { shouldRewrite = true; } else if (payload.version === DM_PRIVACY_SETTINGS_STORAGE_VERSION) { const defaults = createDefaultDmPrivacySettings(); const entries = payload.entries && typeof payload.entries === "object" ? payload.entries : {}; for (const [key, entry] of Object.entries(entries)) { if (!entry || typeof entry !== "object") { shouldRewrite = true; continue; } const overrides = entry.overrides && typeof entry.overrides === "object" ? entry.overrides : {}; const storeKey = key === "default" ? GLOBAL_DM_PRIVACY_PROFILE_KEY : getDmPrivacyStoreKeyForPubkey(key); if (!storeKey) { shouldRewrite = true; continue; } const settings = { readReceiptsEnabled: typeof overrides.readReceiptsEnabled === "boolean" ? overrides.readReceiptsEnabled : defaults.readReceiptsEnabled, typingIndicatorsEnabled: typeof overrides.typingIndicatorsEnabled === "boolean" ? overrides.typingIndicatorsEnabled : defaults.typingIndicatorsEnabled, }; dmPrivacySettingsStore.set(storeKey, settings); } } else { shouldRewrite = true; } } catch (error) { userLogger.warn( "[cache.loadDmPrivacySettingsFromStorage] Failed to parse payload:", error, ); shouldRewrite = true; } if (shouldRewrite) { persistDmPrivacySettingsToStorage(); } return getDmPrivacySettings(); } function persistDmPrivacySettingsToStorage() { if (typeof localStorage === "undefined") { return; } const defaults = createDefaultDmPrivacySettings(); const entries = {}; for (const [key, value] of dmPrivacySettingsStore.entries()) { const settings = value && typeof value === "object" ? value : createDefaultDmPrivacySettings(); const overrides = {}; if (settings.readReceiptsEnabled !== defaults.readReceiptsEnabled) { overrides.readReceiptsEnabled = settings.readReceiptsEnabled; } if (settings.typingIndicatorsEnabled !== defaults.typingIndicatorsEnabled) { overrides.typingIndicatorsEnabled = settings.typingIndicatorsEnabled; } if (Object.keys(overrides).length === 0) { continue; } const storageKey = key === GLOBAL_DM_PRIVACY_PROFILE_KEY ? "default" : key; entries[storageKey] = { overrides, savedAt: Date.now(), }; } if (Object.keys(entries).length === 0) { try { localStorage.removeItem(DM_PRIVACY_SETTINGS_STORAGE_KEY); } catch (error) { userLogger.warn( "[cache.persistDmPrivacySettingsToStorage] Failed to clear overrides:", error, ); } return; } const payload = { version: DM_PRIVACY_SETTINGS_STORAGE_VERSION, entries, savedAt: Date.now(), }; try { localStorage.setItem( DM_PRIVACY_SETTINGS_STORAGE_KEY, JSON.stringify(payload), ); } catch (error) { userLogger.warn( "[cache.persistDmPrivacySettingsToStorage] Failed to persist overrides:", error, ); } } export function setModerationSettings(partial = {}, { persist = true } = {}) { const key = getActiveModerationStoreKey(); const defaults = createDefaultModerationSettings(); const current = ensureModerationSettingsForKey(key); const next = { ...current }; let changed = false; if (Object.prototype.hasOwnProperty.call(partial, "blurThreshold")) { const value = partial.blurThreshold; const sanitized = value === null ? defaults.blurThreshold : sanitizeModerationThreshold(value, defaults.blurThreshold); if (next.blurThreshold !== sanitized) { next.blurThreshold = sanitized; changed = true; } } if (Object.prototype.hasOwnProperty.call(partial, "autoplayBlockThreshold")) { const value = partial.autoplayBlockThreshold; const sanitized = value === null ? defaults.autoplayBlockThreshold : sanitizeModerationThreshold(value, defaults.autoplayBlockThreshold); if (next.autoplayBlockThreshold !== sanitized) { next.autoplayBlockThreshold = sanitized; changed = true; } } if (Object.prototype.hasOwnProperty.call(partial, "trustedMuteHideThreshold")) { const value = partial.trustedMuteHideThreshold; const sanitized = value === null ? defaults.trustedMuteHideThreshold : sanitizeModerationThreshold(value, defaults.trustedMuteHideThreshold); if (next.trustedMuteHideThreshold !== sanitized) { next.trustedMuteHideThreshold = sanitized; changed = true; } } if ( Object.prototype.hasOwnProperty.call(partial, "trustedMuteHideThresholds") ) { const value = partial.trustedMuteHideThresholds; const sanitized = value === null ? defaults.trustedMuteHideThresholds : sanitizeModerationThresholdMap(value, defaults.trustedMuteHideThresholds, { mergeFallback: true, }); const current = next.trustedMuteHideThresholds && typeof next.trustedMuteHideThresholds === "object" ? next.trustedMuteHideThresholds : {}; const nextKeys = new Set([ ...Object.keys(current), ...Object.keys(sanitized), ]); let mapChanged = false; for (const key of nextKeys) { if (current[key] !== sanitized[key]) { mapChanged = true; break; } } if (mapChanged) { next.trustedMuteHideThresholds = { ...sanitized }; changed = true; } } if (Object.prototype.hasOwnProperty.call(partial, "trustedSpamHideThreshold")) { const value = partial.trustedSpamHideThreshold; const sanitized = value === null ? defaults.trustedSpamHideThreshold : sanitizeModerationThreshold(value, defaults.trustedSpamHideThreshold); if (next.trustedSpamHideThreshold !== sanitized) { next.trustedSpamHideThreshold = sanitized; changed = true; } } if (!changed) { if (persist) { persistModerationSettingsToStorage(); } return ensureModerationSettingsForKey(key); } moderationSettingsStore.set(key, next); if (persist) { persistModerationSettingsToStorage(); } return ensureModerationSettingsForKey(key); } export function resetModerationSettings({ persist = true } = {}) { const key = getActiveModerationStoreKey(); const previous = ensureModerationSettingsForKey(key); const defaults = createDefaultModerationSettings(); moderationSettingsStore.set(key, defaults); const changed = haveModerationSettingsChanged(previous, defaults); if (persist) { persistModerationSettingsToStorage(); } if (!changed) { return ensureModerationSettingsForKey(key); } return ensureModerationSettingsForKey(key); } 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.key) || findModerationOverrideByEventId(normalized.eventId); if (!entry) { return null; } return { ...entry }; } export function loadModerationOverridesFromStorage() { if (typeof localStorage === "undefined") { return; } const raw = localStorage.getItem(MODERATION_OVERRIDE_STORAGE_KEY); if (!raw) { return; } let shouldRewrite = false; try { const payload = JSON.parse(raw); if (!payload || typeof payload !== "object") { return; } const entries = []; if (payload.version === MODERATION_OVERRIDE_STORAGE_VERSION) { if (Array.isArray(payload.entries)) { entries.push(...payload.entries); } else { shouldRewrite = true; } } else { return; } moderationOverrides.clear(); for (const entry of entries) { 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(); moderationOverrides.set(normalized.key, { eventId: normalized.eventId, authorPubkey: normalized.authorPubkey, showAnyway: true, updatedAt, }); } } catch (error) { moderationOverrides.clear(); userLogger.warn( "[cache.loadModerationOverridesFromStorage] Failed to parse payload:", error, ); return; } if (shouldRewrite) { persistModerationOverridesToStorage(); } } export function persistModerationOverridesToStorage() { if (typeof localStorage === "undefined") { return; } 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.push({ eventId: normalized.eventId, authorPubkey: normalized.authorPubkey, showAnyway: true, updatedAt, }); } if (entries.length === 0) { try { localStorage.removeItem(MODERATION_OVERRIDE_STORAGE_KEY); } catch (error) { userLogger.warn( "[cache.persistModerationOverridesToStorage] Failed to clear overrides:", error, ); } return; } const payload = { version: MODERATION_OVERRIDE_STORAGE_VERSION, savedAt: Date.now(), entries, }; try { localStorage.setItem( MODERATION_OVERRIDE_STORAGE_KEY, JSON.stringify(payload), ); } catch (error) { userLogger.warn( "[cache.persistModerationOverridesToStorage] Failed to persist overrides:", error, ); } } export function setModerationOverride( eventIdOrDescriptor, override = {}, { persist = true } = {}, ) { const normalized = normalizeModerationOverrideDescriptor( eventIdOrDescriptor, override?.authorPubkey, ); if (!normalized) { return null; } const showAnyway = override?.showAnyway === true; if (!showAnyway) { const removed = moderationOverrides.delete(normalized.key); if (removed && persist) { persistModerationOverridesToStorage(); } return null; } const updatedAt = Number.isFinite(override?.updatedAt) ? Math.floor(override.updatedAt) : Date.now(); 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.eventId !== nextEntry.eventId || existing.authorPubkey !== nextEntry.authorPubkey; moderationOverrides.set(normalized.key, nextEntry); if (persist && changed) { persistModerationOverridesToStorage(); } return { ...nextEntry }; } export function clearModerationOverride( eventIdOrDescriptor, { persist = true } = {}, ) { const normalized = normalizeModerationOverrideDescriptor(eventIdOrDescriptor); if (!normalized) { return false; } 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(); } return removed; } // ---- Account-level ("trusted creator") web-of-trust overrides --------------- // Keyed by author hex; a showAnyway entry suppresses the WoT blur/hide/warning // for EVERY video by that author until reset. Mirrors the per-video store above. function normalizeAuthorOverrideHex(pubkey) { const hex = normalizeHexPubkey(pubkey); return hex ? hex.toLowerCase() : ""; } export function getAuthorModerationOverridesList() { return Array.from(authorModerationOverrides.values()).map((entry) => ({ ...entry, })); } export function getAuthorModerationOverride(pubkey) { const hex = normalizeAuthorOverrideHex(pubkey); if (!hex) { return null; } const entry = authorModerationOverrides.get(hex); return entry ? { ...entry } : null; } export function hasAuthorModerationOverride(pubkey) { const hex = normalizeAuthorOverrideHex(pubkey); if (!hex) { return false; } return authorModerationOverrides.get(hex)?.showAnyway === true; } export function setAuthorModerationOverride( pubkey, override = {}, { persist = true } = {}, ) { const hex = normalizeAuthorOverrideHex(pubkey); if (!hex) { return null; } const showAnyway = override?.showAnyway === true; if (!showAnyway) { const removed = authorModerationOverrides.delete(hex); if (removed && persist) { persistAuthorModerationOverridesToStorage(); } return null; } const updatedAt = Number.isFinite(override?.updatedAt) ? Math.floor(override.updatedAt) : Date.now(); const existing = authorModerationOverrides.get(hex); const nextEntry = { authorPubkey: hex, showAnyway: true, updatedAt }; const changed = !existing || existing.showAnyway !== true || existing.updatedAt !== updatedAt; authorModerationOverrides.set(hex, nextEntry); if (persist && changed) { persistAuthorModerationOverridesToStorage(); } return { ...nextEntry }; } export function clearAuthorModerationOverride(pubkey, { persist = true } = {}) { const hex = normalizeAuthorOverrideHex(pubkey); if (!hex) { return false; } const removed = authorModerationOverrides.delete(hex); if (removed && persist) { persistAuthorModerationOverridesToStorage(); } return removed; } export function loadAuthorModerationOverridesFromStorage() { if (typeof localStorage === "undefined") { return; } const raw = localStorage.getItem(AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY); if (!raw) { return; } try { const payload = JSON.parse(raw); if ( !payload || typeof payload !== "object" || payload.version !== AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION || !Array.isArray(payload.entries) ) { return; } authorModerationOverrides.clear(); for (const entry of payload.entries) { if (!entry || entry.showAnyway !== true) { continue; } const hex = normalizeAuthorOverrideHex(entry.authorPubkey || entry.pubkey); if (!hex) { continue; } const updatedAt = Number.isFinite(entry.updatedAt) ? Math.floor(entry.updatedAt) : Date.now(); authorModerationOverrides.set(hex, { authorPubkey: hex, showAnyway: true, updatedAt, }); } } catch (error) { authorModerationOverrides.clear(); userLogger.warn( "[cache.loadAuthorModerationOverridesFromStorage] Failed to parse payload:", error, ); } } export function persistAuthorModerationOverridesToStorage() { if (typeof localStorage === "undefined") { return; } const entries = []; for (const entry of authorModerationOverrides.values()) { if (!entry || entry.showAnyway !== true) { continue; } entries.push({ authorPubkey: entry.authorPubkey, showAnyway: true, updatedAt: Number.isFinite(entry.updatedAt) ? Math.floor(entry.updatedAt) : Date.now(), }); } try { if (entries.length === 0) { localStorage.removeItem(AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY); return; } localStorage.setItem( AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY, JSON.stringify({ version: AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION, savedAt: Date.now(), entries, }), ); } catch (error) { userLogger.warn( "[cache.persistAuthorModerationOverridesToStorage] Failed to persist overrides:", error, ); } } function buildUrlProbeKey(url, options = {}) { const trimmed = typeof url === "string" ? url : ""; const mode = options?.confirmPlayable ? "playable" : "basic"; return `${trimmed}::${mode}`; } function isUrlHealthEntryFresh(entry, url) { if (!entry || typeof entry !== "object") { return false; } const now = Date.now(); if (typeof entry.expiresAt !== "number" || entry.expiresAt <= now) { return false; } if (url && entry.url && entry.url !== url) { return false; } return true; } export function getCachedUrlHealth(eventId, url) { if (!eventId) { return null; } const entry = urlHealthCache.get(eventId); if (isUrlHealthEntryFresh(entry, url)) { return entry; } if (entry) { urlHealthCache.delete(eventId); } const stored = readUrlHealthFromStorage(eventId); if (!isUrlHealthEntryFresh(stored, url)) { if (stored) { removeUrlHealthFromStorage(eventId); } return null; } urlHealthCache.set(eventId, stored); return stored; } export function storeUrlHealth(eventId, url, result, ttlMs = URL_HEALTH_TTL_MS) { if (!eventId) { return null; } const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : URL_HEALTH_TTL_MS; const now = Date.now(); const entry = { status: result?.status || "checking", message: result?.message || "⏳ CDN", url: url || result?.url || "", expiresAt: now + ttl, lastCheckedAt: now, }; urlHealthCache.set(eventId, entry); writeUrlHealthToStorage(eventId, entry); return entry; } export function setInFlightUrlProbe(eventId, url, promise, options = {}) { if (!eventId || !promise) { return; } const key = buildUrlProbeKey(url, options); urlHealthInFlight.set(eventId, { promise, key }); promise.finally(() => { const current = urlHealthInFlight.get(eventId); if (current && current.promise === promise) { urlHealthInFlight.delete(eventId); } }); } export function getInFlightUrlProbe(eventId, url, options = {}) { if (!eventId) { return null; } const entry = urlHealthInFlight.get(eventId); if (!entry) { return null; } const key = buildUrlProbeKey(url, options); if (entry.key && entry.key !== key) { return null; } return entry.promise; } export const urlHealthConstants = { URL_HEALTH_TTL_MS, URL_HEALTH_TIMEOUT_RETRY_MS, URL_PROBE_TIMEOUT_RETRY_MS, };