Merge pull request #2288 from PR0M3TH3AN/decompose-profile-modal-3920695150590991705

[decompose] Extract profile modal logic to reduce file size
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-09 23:08:37 -05:00
commit fc25801811
3 files changed
+930 -898

No files matched your search

+916
View File
@@ -0,0 +1,916 @@
import {
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
DEFAULT_BLUR_THRESHOLD,
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
} from "../constants.js";
const noop = () => {};
export const NWC_URI_SCHEME = "nostr+walletconnect://";
const PROVIDER_BADGE_BASE_CLASS =
"text-3xs font-semibold uppercase tracking-extra-wide";
const PROVIDER_BADGE_VARIANT_CLASS_MAP = Object.freeze({
info: "text-status-info",
success: "text-status-success",
warning: "text-status-warning",
danger: "text-status-danger",
neutral: "text-muted",
accent: "text-accent",
primary: "text-accent",
});
function resolveProviderBadgeClass(variant) {
if (typeof variant === "string" && variant.trim()) {
const normalized = variant.trim().toLowerCase();
if (Object.prototype.hasOwnProperty.call(PROVIDER_BADGE_VARIANT_CLASS_MAP, normalized)) {
return PROVIDER_BADGE_VARIANT_CLASS_MAP[normalized];
}
}
return PROVIDER_BADGE_VARIANT_CLASS_MAP.neutral;
}
const DEFAULT_INTERNAL_NWC_SETTINGS = Object.freeze({
nwcUri: "",
defaultZap: null,
lastChecked: null,
});
function createInternalDefaultNwcSettings() {
return { ...DEFAULT_INTERNAL_NWC_SETTINGS };
}
const DEFAULT_INTERNAL_MODERATION_SETTINGS = Object.freeze({
blurThreshold: DEFAULT_BLUR_THRESHOLD,
autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
});
function createInternalDefaultModerationSettings() {
return { ...DEFAULT_INTERNAL_MODERATION_SETTINGS };
}
const DEFAULT_INTERNAL_DM_PRIVACY_SETTINGS = Object.freeze({
readReceiptsEnabled: false,
typingIndicatorsEnabled: false,
});
function createInternalDefaultDmPrivacySettings() {
return { ...DEFAULT_INTERNAL_DM_PRIVACY_SETTINGS };
}
function ensureInternalModerationSettings(internalState) {
if (!internalState || typeof internalState !== "object") {
return createInternalDefaultModerationSettings();
}
if (
!internalState.moderationSettings ||
typeof internalState.moderationSettings !== "object" ||
internalState.moderationSettings === null
) {
internalState.moderationSettings = createInternalDefaultModerationSettings();
}
return internalState.moderationSettings;
}
function ensureInternalWalletSettings(internalState) {
if (!internalState || typeof internalState !== "object") {
return createInternalDefaultNwcSettings();
}
if (
!internalState.walletSettings ||
typeof internalState.walletSettings !== "object" ||
internalState.walletSettings === null
) {
internalState.walletSettings = createInternalDefaultNwcSettings();
}
return internalState.walletSettings;
}
const SERVICE_CONTRACT = [
{
key: "normalizeHexPubkey",
type: "function",
description:
"Normalizes a provided pubkey (hex or npub) so profile lookups are stable.",
fallback: () => (value) => {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return trimmed.toLowerCase();
}
return trimmed;
},
},
{
key: "safeEncodeNpub",
type: "function",
description:
"Encodes a hex pubkey as an npub for display. Expected to be resilient to bad input.",
fallback: () => (pubkey) => {
if (typeof pubkey === "string" && pubkey.trim()) {
return pubkey.trim();
}
return null;
},
},
{
key: "safeDecodeNpub",
type: "function",
description:
"Decodes an npub back to its hex form. Should return null/undefined for invalid payloads.",
fallback: () => () => null,
},
{
key: "truncateMiddle",
type: "function",
description:
"Utility used for shortening long identifiers in the modal without losing context.",
fallback: () => (value) => value,
},
{
key: "formatShortNpub",
type: "function",
description:
"Formats npub strings for display using the canonical short representation (npubXXXX…XXXX).",
fallback: () => (value) => (typeof value === "string" ? value : ""),
},
{
key: "requestAddProfileLogin",
type: "function",
description:
"Opens the login modal and resolves with the authentication result for the add-profile flow.",
fallback: () => async () => {
throw new Error("Login service unavailable.");
},
},
{
key: "describeLoginError",
type: "function",
description:
"Maps authentication errors to human-readable strings for add-profile messaging.",
fallback: () => (_, fallbackMessage) =>
typeof fallbackMessage === "string" && fallbackMessage.trim()
? fallbackMessage.trim()
: "Failed to login. Please try again.",
},
{
key: "r2Service",
type: "object",
description: "Service for handling file uploads (R2/S3).",
optional: true,
},
{
key: "nostrService",
type: "object",
description:
"Provides encrypted direct message helpers for loading and subscribing to inbox updates.",
optional: true,
fallback: () => ({
getDirectMessages: () => [],
loadDirectMessages: async () => [],
ensureDirectMessageSubscription: () => null,
stopDirectMessageSubscription: () => {},
clearDirectMessages: () => {},
on: () => () => {},
}),
},
{
key: "hashtagPreferences",
type: "object",
description:
"Service providing access to interest and disinterest hashtag collections.",
fallback: () => ({
getInterests: () => [],
getDisinterests: () => [],
addInterest: () => false,
removeInterest: () => false,
addDisinterest: () => false,
removeDisinterest: () => false,
publish: async () => false,
on: () => () => {},
}),
},
{
key: "getHashtagPreferences",
type: "function",
description:
"Returns the cached hashtag preferences snapshot (interests, disinterests, metadata).",
fallback: ({ resolved }) => () => {
const service = resolved.hashtagPreferences || {};
const interests =
typeof service.getInterests === "function"
? service.getInterests()
: [];
const disinterests =
typeof service.getDisinterests === "function"
? service.getDisinterests()
: [];
return {
interests,
disinterests,
eventId: null,
createdAt: null,
loaded: false,
};
},
},
{
key: "describeHashtagPreferencesError",
type: "function",
description:
"Maps hashtag preference errors to user-facing status messages for the modal.",
fallback: () => (_, fallbackMessage) =>
typeof fallbackMessage === "string" && fallbackMessage.trim()
? fallbackMessage.trim()
: "Failed to update hashtag preferences. Please try again.",
},
{
key: "getProfileCacheEntry",
type: "function",
description:
"Returns the cached profile metadata for a normalized pubkey (if available).",
fallback: () => () => null,
},
{
key: "batchFetchProfiles",
type: "function",
description:
"Fetches and caches profile metadata for a set of pubkeys so the modal can hydrate avatars/names in bulk.",
fallback: () => async () => [],
},
{
key: "fetchDmRelayHints",
type: "function",
description:
"Fetches DM relay hints (kind 10050) for a given pubkey.",
fallback: () => async () => [],
},
{
key: "switchProfile",
type: "function",
description:
"Switches the active application profile to the provided pubkey and updates state accordingly.",
fallback: () => async () => ({ switched: false }),
},
{
key: "removeSavedProfile",
type: "function",
description:
"Removes a saved profile entry and persists the updated collection to storage.",
fallback: () => () => ({ removed: false }),
},
{
key: "relayManager",
type: "object",
description:
"Shared relay manager instance responsible for the user\'s relay configuration.",
},
{
key: "userBlocks",
type: "object",
description: "User blocks helper used to read and mutate the local block list.",
},
{
key: "nostrClient",
type: "object",
description: "Reference to the nostr client powering subscriptions and profile fetches.",
},
{
key: "subscriptions",
type: "object",
optional: true,
description:
"Subscriptions manager used by the profile modal to inspect or refresh the viewer's channel subscriptions.",
},
{
key: "moderation",
type: "object",
optional: true,
description:
"Moderation service exposing the viewer's Nostr contacts and trust graph metadata.",
},
{
key: "accessControl",
type: "object",
description:
"Access control module required for admin list permission checks and updates.",
},
{
key: "getCurrentUserNpub",
type: "function",
description: "Returns the active user\'s npub so UI actions can target the correct actor.",
fallback: () => () => null,
},
{
key: "nwcSettings",
type: "object",
description:
"Service that manages Nostr Wallet Connect settings and interactions for the active profile.",
fallback: ({ internalState }) => {
const ensure = () => ensureInternalWalletSettings(internalState);
const service = {};
service.createDefaultNwcSettings = () => createInternalDefaultNwcSettings();
service.getActiveNwcSettings = () => {
const current = ensure();
return { ...current };
};
service.updateActiveNwcSettings = (partial = {}) => {
const current = ensure();
const next = {
...current,
...(partial && typeof partial === "object" ? partial : {}),
};
internalState.walletSettings = next;
return { ...next };
};
service.hydrateNwcSettingsForPubkey = async () => {
return service.getActiveNwcSettings();
};
service.handleProfileWalletPersist = async (options = {}) => {
const { nwcUri, defaultZap, lastChecked } =
options && typeof options === "object" ? options : {};
const partial = {};
if (nwcUri !== undefined) {
partial.nwcUri = nwcUri;
}
if (defaultZap !== undefined) {
partial.defaultZap = defaultZap;
}
if (lastChecked !== undefined) {
partial.lastChecked = lastChecked;
}
if (!Object.keys(partial).length) {
return service.getActiveNwcSettings();
}
return service.updateActiveNwcSettings(partial);
};
service.hasActiveWalletConnection = () => {
const settings = ensure();
const candidate =
typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : "";
return candidate.length > 0;
};
service.validateWalletUri = (uri, { requireValue = false } = {}) => {
const value = typeof uri === "string" ? uri.trim() : "";
if (!value) {
if (requireValue) {
return {
valid: false,
sanitized: "",
message: "Enter a wallet connect URI before continuing.",
};
}
return { valid: true, sanitized: "" };
}
if (!value.toLowerCase().startsWith(NWC_URI_SCHEME)) {
return {
valid: false,
sanitized: value,
message: `Wallet URI must start with ${NWC_URI_SCHEME}.`,
};
}
return { valid: true, sanitized: value };
};
service.ensureWallet = async ({ nwcUri } = {}) => {
const settings = service.getActiveNwcSettings();
const candidate =
typeof nwcUri === "string" && nwcUri.trim()
? nwcUri.trim()
: settings.nwcUri || "";
const { valid, sanitized, message } = service.validateWalletUri(
candidate,
{ requireValue: true },
);
if (!valid) {
throw new Error(message || "Invalid wallet URI provided.");
}
return { ...settings, nwcUri: sanitized };
};
service.clearStoredNwcSettings = async () => true;
service.onLogin = async () => service.getActiveNwcSettings();
service.onLogout = async () => service.createDefaultNwcSettings();
service.onIdentityChanged = () => {
internalState.walletSettings = createInternalDefaultNwcSettings();
};
return service;
},
},
{
key: "moderationSettings",
type: "object",
description:
"Service that manages Safety & Moderation threshold overrides for the active profile.",
fallback: ({ internalState }) => {
const ensure = () => ensureInternalModerationSettings(internalState);
const sanitize = (value, fallback) => {
if (value === null || value === undefined) {
return fallback;
}
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return Math.max(0, Math.floor(numeric));
};
const service = {};
service.getDefaultModerationSettings = () =>
createInternalDefaultModerationSettings();
service.getActiveModerationSettings = () => {
const current = ensure();
const defaults = service.getDefaultModerationSettings();
return {
blurThreshold: sanitize(current.blurThreshold, defaults.blurThreshold),
autoplayBlockThreshold: sanitize(
current.autoplayBlockThreshold,
defaults.autoplayBlockThreshold,
),
};
};
service.updateModerationSettings = (partial = {}) => {
const defaults = service.getDefaultModerationSettings();
const current = ensure();
const next = { ...current };
if (Object.prototype.hasOwnProperty.call(partial, "blurThreshold")) {
const value = partial.blurThreshold;
next.blurThreshold =
value === null
? defaults.blurThreshold
: sanitize(value, defaults.blurThreshold);
}
if (
Object.prototype.hasOwnProperty.call(
partial,
"autoplayBlockThreshold",
)
) {
const value = partial.autoplayBlockThreshold;
next.autoplayBlockThreshold =
value === null
? defaults.autoplayBlockThreshold
: sanitize(value, defaults.autoplayBlockThreshold);
}
internalState.moderationSettings = next;
return service.getActiveModerationSettings();
};
service.resetModerationSettings = () => {
internalState.moderationSettings = createInternalDefaultModerationSettings();
return service.getActiveModerationSettings();
};
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",
description: "Triggers a video reload so UI reflects profile or permission changes.",
fallback: () => async () => undefined,
},
{
key: "onVideosShouldRefresh",
type: "function",
description:
"Signals that video listings should be refreshed after a profile-driven mutation.",
fallback: ({ services, resolved }) => async (context = {}) => {
const loader =
typeof services.loadVideos === "function"
? services.loadVideos
: typeof resolved.loadVideos === "function"
? resolved.loadVideos
: null;
if (loader) {
return loader(true, context);
}
return undefined;
},
},
{
key: "sendAdminListNotification",
type: "function",
description:
"Sends an administrative notification when moderators/whitelist/blacklist entries change.",
optional: true,
},
{
key: "describeAdminError",
type: "function",
description:
"Maps low-level admin errors to readable copy for surfaced error messages.",
fallback: () => () => "",
},
{
key: "describeNotificationError",
type: "function",
description:
"Maps notification send errors to human readable descriptions for toast/banner messaging.",
fallback: () => () => "",
},
{
key: "onAccessControlUpdated",
type: "function",
description:
"Callback invoked after access control mutations so the app can refresh dependent UI.",
fallback: () => async () => undefined,
},
{
key: "persistSavedProfiles",
type: "function",
description:
"Persists the saved profile collection (and optionally the active profile) to storage.",
fallback: () => () => undefined,
},
];
const STATE_CONTRACT = [
{
key: "getSavedProfiles",
type: "function",
description:
"Returns the saved profile entries that populate the account switcher UI.",
fallback: (internal) => () => internal.savedProfiles.slice(),
},
{
key: "setSavedProfiles",
type: "function",
description:
"Replaces the saved profile entries and returns the newly stored collection.",
fallback: (internal) => (profiles = []) => {
internal.savedProfiles = Array.isArray(profiles)
? profiles.slice()
: [];
return internal.savedProfiles;
},
},
{
key: "persistSavedProfiles",
type: "function",
description:
"Persists the saved profile collection (and optionally the active profile) to storage.",
fallback: () => noop,
},
{
key: "getActivePubkey",
type: "function",
description: "Returns the currently active profile pubkey.",
fallback: (internal) => () => internal.activePubkey,
},
{
key: "setActivePubkey",
type: "function",
description: "Updates the active profile pubkey and returns the stored value.",
fallback: (internal) => (pubkey) => {
if (typeof pubkey === "string") {
const trimmed = pubkey.trim();
internal.activePubkey = trimmed ? trimmed : null;
} else {
internal.activePubkey = null;
}
return internal.activePubkey;
},
},
{
key: "getCachedSelection",
type: "function",
description:
"Reads the cached profile selection used to restore switcher focus after reloads.",
fallback: (internal) => () => internal.cachedSelection,
},
{
key: "setCachedSelection",
type: "function",
description:
"Caches the last selected profile identifier and returns the stored value.",
fallback: (internal) => (value) => {
if (typeof value === "string") {
const trimmed = value.trim();
internal.cachedSelection = trimmed || null;
} else {
internal.cachedSelection = null;
}
return internal.cachedSelection;
},
},
{
key: "getActivePane",
type: "function",
description: "Returns the identifier for the currently visible profile modal pane.",
fallback: (internal) => () => internal.activePane,
},
{
key: "setActivePane",
type: "function",
description: "Updates the active pane identifier and returns the stored value.",
fallback: (internal) => (pane) => {
if (typeof pane === "string") {
const trimmed = pane.trim().toLowerCase();
internal.activePane = trimmed || "account";
} else {
internal.activePane = "account";
}
return internal.activePane;
},
},
{
key: "getWalletBusy",
type: "function",
description:
"Indicates whether wallet-related actions are currently in-flight (disables UI as needed).",
fallback: (internal) => () => internal.walletBusy,
},
{
key: "setWalletBusy",
type: "function",
description: "Updates the wallet busy flag and returns the new boolean state.",
fallback: (internal) => (isBusy) => {
internal.walletBusy = Boolean(isBusy);
return internal.walletBusy;
},
},
{
key: "getDmRecipient",
type: "function",
description:
"Returns the currently selected direct message recipient pubkey.",
fallback: (internal) => () => internal.dmRecipient,
},
{
key: "setDmRecipient",
type: "function",
description:
"Updates the selected direct message recipient pubkey and returns the stored value.",
fallback: (internal) => (pubkey) => {
if (typeof pubkey === "string") {
const trimmed = pubkey.trim();
internal.dmRecipient = trimmed || null;
} else {
internal.dmRecipient = null;
}
return internal.dmRecipient;
},
},
{
key: "getDmRelayHints",
type: "function",
description:
"Returns the cached DM relay hints for a given pubkey.",
fallback: (internal) => (pubkey) => {
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized || !(internal.dmRelayHints instanceof Map)) {
return [];
}
const hints = internal.dmRelayHints.get(normalized);
return Array.isArray(hints) ? hints.slice() : [];
},
},
{
key: "setDmRelayHints",
type: "function",
description:
"Stores DM relay hints for a given pubkey and returns the stored list.",
fallback: (internal) => (pubkey, hints = []) => {
if (!(internal.dmRelayHints instanceof Map)) {
internal.dmRelayHints = new Map();
}
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized) {
return [];
}
const stored = Array.isArray(hints) ? hints.slice() : [];
internal.dmRelayHints.set(normalized, stored);
return stored;
},
},
{
key: "getDmRelayPreferences",
type: "function",
description:
"Returns the cached DM relay hints for a given pubkey (publisher view).",
fallback: (internal) => (pubkey) => {
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized || !(internal.dmRelayHints instanceof Map)) {
return [];
}
const hints = internal.dmRelayHints.get(normalized);
return Array.isArray(hints) ? hints.slice() : [];
},
},
{
key: "setDmRelayPreferences",
type: "function",
description:
"Stores DM relay hints for a given pubkey (publisher view).",
fallback: (internal) => (pubkey, hints = []) => {
if (!(internal.dmRelayHints instanceof Map)) {
internal.dmRelayHints = new Map();
}
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized) {
return [];
}
const stored = Array.isArray(hints) ? hints.slice() : [];
internal.dmRelayHints.set(normalized, stored);
return stored;
},
},
{
key: "getDmPrivacySettings",
type: "function",
description:
"Returns the DM privacy settings (read receipts and typing indicators) for the active profile.",
fallback: (internal) => () => {
if (!(internal.dmPrivacySettings instanceof Map)) {
internal.dmPrivacySettings = new Map();
}
const settings = internal.dmPrivacySettings.get("default");
return settings ? { ...settings } : createInternalDefaultDmPrivacySettings();
},
},
{
key: "setDmPrivacySettings",
type: "function",
description:
"Updates the DM privacy settings (read receipts and typing indicators) for the active profile.",
fallback: (internal) => (settings = {}) => {
if (!(internal.dmPrivacySettings instanceof Map)) {
internal.dmPrivacySettings = new Map();
}
const base = createInternalDefaultDmPrivacySettings();
const normalized = {
readReceiptsEnabled:
typeof settings.readReceiptsEnabled === "boolean"
? settings.readReceiptsEnabled
: base.readReceiptsEnabled,
typingIndicatorsEnabled:
typeof settings.typingIndicatorsEnabled === "boolean"
? settings.typingIndicatorsEnabled
: base.typingIndicatorsEnabled,
};
internal.dmPrivacySettings.set("default", normalized);
return { ...normalized };
},
},
];
function buildServicesContract(services = {}, internalState) {
const resolved = {};
const missing = [];
SERVICE_CONTRACT.forEach((definition) => {
const provided = services[definition.key];
if (provided == null) {
if (typeof definition.fallback === "function") {
const fallbackValue = definition.fallback({
services,
resolved,
internalState,
definition,
});
if (definition.type === "function" && typeof fallbackValue === "function") {
resolved[definition.key] = fallbackValue;
return;
}
if (
definition.type === "object" &&
typeof fallbackValue === "object" &&
fallbackValue !== null
) {
resolved[definition.key] = fallbackValue;
return;
}
}
if (definition.optional) {
return;
}
missing.push(`- ${definition.key}: ${definition.description}`);
return;
}
if (definition.type === "function" && typeof provided !== "function") {
throw new TypeError(
`Expected service \"${definition.key}\" to be a function. Received ${typeof provided}.`,
);
}
if (
definition.type === "object" &&
(typeof provided !== "object" || provided === null)
) {
throw new TypeError(
`Expected service \"${definition.key}\" to be an object. Received ${typeof provided}.`,
);
}
resolved[definition.key] = provided;
});
if (missing.length) {
throw new Error(
[
"[ProfileModalController] Missing required services for profile modal controller:",
...missing,
].join("\n"),
);
}
return Object.freeze({ ...services, ...resolved });
}
function buildStateContract(state = {}, internalState) {
const resolved = {};
STATE_CONTRACT.forEach((definition) => {
const value = state[definition.key];
if (typeof value === definition.type) {
resolved[definition.key] = value;
return;
}
if (definition.fallback) {
const fallback = definition.fallback(internalState);
if (typeof fallback === definition.type) {
resolved[definition.key] = fallback;
return;
}
}
throw new Error(
`[ProfileModalController] Missing state handler \"${definition.key}\" (${definition.description}).`,
);
});
return { ...state, ...resolved };
}
export {
PROVIDER_BADGE_BASE_CLASS,
PROVIDER_BADGE_VARIANT_CLASS_MAP,
DEFAULT_INTERNAL_NWC_SETTINGS,
DEFAULT_INTERNAL_MODERATION_SETTINGS,
DEFAULT_INTERNAL_DM_PRIVACY_SETTINGS,
resolveProviderBadgeClass,
createInternalDefaultNwcSettings,
createInternalDefaultModerationSettings,
createInternalDefaultDmPrivacySettings,
ensureInternalModerationSettings,
ensureInternalWalletSettings,
buildServicesContract,
buildStateContract
};
+13 -897
View File
@@ -6,13 +6,20 @@ import {
MAX_WALLET_DEFAULT_ZAP as CONFIG_MAX_WALLET_DEFAULT_ZAP,
} from "../config.js";
import { normalizeDesignSystemContext } from "../designSystem.js";
import { RUNTIME_FLAGS } from "../constants.js";
import {
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
DEFAULT_BLUR_THRESHOLD,
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
RUNTIME_FLAGS,
} from "../constants.js";
PROVIDER_BADGE_BASE_CLASS,
DEFAULT_INTERNAL_MODERATION_SETTINGS,
resolveProviderBadgeClass,
createInternalDefaultNwcSettings,
createInternalDefaultModerationSettings,
createInternalDefaultDmPrivacySettings,
ensureInternalModerationSettings,
ensureInternalWalletSettings,
buildServicesContract,
buildStateContract,
NWC_URI_SCHEME
} from "./profileModalContract.js";
import { getBreakpointLg } from "../designSystem/metrics.js";
import { getProviderMetadata } from "../services/authProviders/index.js";
import { AppShell } from "./dm/index.js";
@@ -53,7 +60,6 @@ const FALLBACK_PROFILE_AVATAR = "assets/svg/default-profile.svg";
const DEFAULT_ADMIN_DM_IMAGE_URL =
"https://beta.bitvid.network/assets/jpg/video-thumbnail-fallback.jpg";
const DEFAULT_BITVID_WEBSITE_URL = "https://bitvid.network/";
const NWC_URI_SCHEME = "nostr+walletconnect://";
const SECRET_PLACEHOLDER = "*****";
const DEFAULT_MAX_WALLET_DEFAULT_ZAP = 100000000;
const DEFAULT_SAVED_PROFILE_LABEL = "Saved profile";
@@ -69,896 +75,6 @@ const ADD_PROFILE_CANCELLATION_CODES = new Set([
"modal-dismissed",
]);
const PROVIDER_BADGE_BASE_CLASS =
"text-3xs font-semibold uppercase tracking-extra-wide";
const PROVIDER_BADGE_VARIANT_CLASS_MAP = Object.freeze({
info: "text-status-info",
success: "text-status-success",
warning: "text-status-warning",
danger: "text-status-danger",
neutral: "text-muted",
accent: "text-accent",
primary: "text-accent",
});
function resolveProviderBadgeClass(variant) {
if (typeof variant === "string" && variant.trim()) {
const normalized = variant.trim().toLowerCase();
if (Object.prototype.hasOwnProperty.call(PROVIDER_BADGE_VARIANT_CLASS_MAP, normalized)) {
return PROVIDER_BADGE_VARIANT_CLASS_MAP[normalized];
}
}
return PROVIDER_BADGE_VARIANT_CLASS_MAP.neutral;
}
const DEFAULT_INTERNAL_NWC_SETTINGS = Object.freeze({
nwcUri: "",
defaultZap: null,
lastChecked: null,
});
function createInternalDefaultNwcSettings() {
return { ...DEFAULT_INTERNAL_NWC_SETTINGS };
}
const DEFAULT_INTERNAL_MODERATION_SETTINGS = Object.freeze({
blurThreshold: DEFAULT_BLUR_THRESHOLD,
autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
});
function createInternalDefaultModerationSettings() {
return { ...DEFAULT_INTERNAL_MODERATION_SETTINGS };
}
const DEFAULT_INTERNAL_DM_PRIVACY_SETTINGS = Object.freeze({
readReceiptsEnabled: false,
typingIndicatorsEnabled: false,
});
function createInternalDefaultDmPrivacySettings() {
return { ...DEFAULT_INTERNAL_DM_PRIVACY_SETTINGS };
}
function ensureInternalModerationSettings(internalState) {
if (!internalState || typeof internalState !== "object") {
return createInternalDefaultModerationSettings();
}
if (
!internalState.moderationSettings ||
typeof internalState.moderationSettings !== "object" ||
internalState.moderationSettings === null
) {
internalState.moderationSettings = createInternalDefaultModerationSettings();
}
return internalState.moderationSettings;
}
function ensureInternalWalletSettings(internalState) {
if (!internalState || typeof internalState !== "object") {
return createInternalDefaultNwcSettings();
}
if (
!internalState.walletSettings ||
typeof internalState.walletSettings !== "object" ||
internalState.walletSettings === null
) {
internalState.walletSettings = createInternalDefaultNwcSettings();
}
return internalState.walletSettings;
}
const SERVICE_CONTRACT = [
{
key: "normalizeHexPubkey",
type: "function",
description:
"Normalizes a provided pubkey (hex or npub) so profile lookups are stable.",
fallback: () => (value) => {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return trimmed.toLowerCase();
}
return trimmed;
},
},
{
key: "safeEncodeNpub",
type: "function",
description:
"Encodes a hex pubkey as an npub for display. Expected to be resilient to bad input.",
fallback: () => (pubkey) => {
if (typeof pubkey === "string" && pubkey.trim()) {
return pubkey.trim();
}
return null;
},
},
{
key: "safeDecodeNpub",
type: "function",
description:
"Decodes an npub back to its hex form. Should return null/undefined for invalid payloads.",
fallback: () => () => null,
},
{
key: "truncateMiddle",
type: "function",
description:
"Utility used for shortening long identifiers in the modal without losing context.",
fallback: () => (value) => value,
},
{
key: "formatShortNpub",
type: "function",
description:
"Formats npub strings for display using the canonical short representation (npubXXXX…XXXX).",
fallback: () => (value) => (typeof value === "string" ? value : ""),
},
{
key: "requestAddProfileLogin",
type: "function",
description:
"Opens the login modal and resolves with the authentication result for the add-profile flow.",
fallback: () => async () => {
throw new Error("Login service unavailable.");
},
},
{
key: "describeLoginError",
type: "function",
description:
"Maps authentication errors to human-readable strings for add-profile messaging.",
fallback: () => (_, fallbackMessage) =>
typeof fallbackMessage === "string" && fallbackMessage.trim()
? fallbackMessage.trim()
: "Failed to login. Please try again.",
},
{
key: "r2Service",
type: "object",
description: "Service for handling file uploads (R2/S3).",
optional: true,
},
{
key: "nostrService",
type: "object",
description:
"Provides encrypted direct message helpers for loading and subscribing to inbox updates.",
optional: true,
fallback: () => ({
getDirectMessages: () => [],
loadDirectMessages: async () => [],
ensureDirectMessageSubscription: () => null,
stopDirectMessageSubscription: () => {},
clearDirectMessages: () => {},
on: () => () => {},
}),
},
{
key: "hashtagPreferences",
type: "object",
description:
"Service providing access to interest and disinterest hashtag collections.",
fallback: () => ({
getInterests: () => [],
getDisinterests: () => [],
addInterest: () => false,
removeInterest: () => false,
addDisinterest: () => false,
removeDisinterest: () => false,
publish: async () => false,
on: () => () => {},
}),
},
{
key: "getHashtagPreferences",
type: "function",
description:
"Returns the cached hashtag preferences snapshot (interests, disinterests, metadata).",
fallback: ({ resolved }) => () => {
const service = resolved.hashtagPreferences || {};
const interests =
typeof service.getInterests === "function"
? service.getInterests()
: [];
const disinterests =
typeof service.getDisinterests === "function"
? service.getDisinterests()
: [];
return {
interests,
disinterests,
eventId: null,
createdAt: null,
loaded: false,
};
},
},
{
key: "describeHashtagPreferencesError",
type: "function",
description:
"Maps hashtag preference errors to user-facing status messages for the modal.",
fallback: () => (_, fallbackMessage) =>
typeof fallbackMessage === "string" && fallbackMessage.trim()
? fallbackMessage.trim()
: "Failed to update hashtag preferences. Please try again.",
},
{
key: "getProfileCacheEntry",
type: "function",
description:
"Returns the cached profile metadata for a normalized pubkey (if available).",
fallback: () => () => null,
},
{
key: "batchFetchProfiles",
type: "function",
description:
"Fetches and caches profile metadata for a set of pubkeys so the modal can hydrate avatars/names in bulk.",
fallback: () => async () => [],
},
{
key: "fetchDmRelayHints",
type: "function",
description:
"Fetches DM relay hints (kind 10050) for a given pubkey.",
fallback: () => async () => [],
},
{
key: "switchProfile",
type: "function",
description:
"Switches the active application profile to the provided pubkey and updates state accordingly.",
fallback: () => async () => ({ switched: false }),
},
{
key: "removeSavedProfile",
type: "function",
description:
"Removes a saved profile entry and persists the updated collection to storage.",
fallback: () => () => ({ removed: false }),
},
{
key: "relayManager",
type: "object",
description:
"Shared relay manager instance responsible for the user\'s relay configuration.",
},
{
key: "userBlocks",
type: "object",
description: "User blocks helper used to read and mutate the local block list.",
},
{
key: "nostrClient",
type: "object",
description: "Reference to the nostr client powering subscriptions and profile fetches.",
},
{
key: "subscriptions",
type: "object",
optional: true,
description:
"Subscriptions manager used by the profile modal to inspect or refresh the viewer's channel subscriptions.",
},
{
key: "moderation",
type: "object",
optional: true,
description:
"Moderation service exposing the viewer's Nostr contacts and trust graph metadata.",
},
{
key: "accessControl",
type: "object",
description:
"Access control module required for admin list permission checks and updates.",
},
{
key: "getCurrentUserNpub",
type: "function",
description: "Returns the active user\'s npub so UI actions can target the correct actor.",
fallback: () => () => null,
},
{
key: "nwcSettings",
type: "object",
description:
"Service that manages Nostr Wallet Connect settings and interactions for the active profile.",
fallback: ({ internalState }) => {
const ensure = () => ensureInternalWalletSettings(internalState);
const service = {};
service.createDefaultNwcSettings = () => createInternalDefaultNwcSettings();
service.getActiveNwcSettings = () => {
const current = ensure();
return { ...current };
};
service.updateActiveNwcSettings = (partial = {}) => {
const current = ensure();
const next = {
...current,
...(partial && typeof partial === "object" ? partial : {}),
};
internalState.walletSettings = next;
return { ...next };
};
service.hydrateNwcSettingsForPubkey = async () => {
return service.getActiveNwcSettings();
};
service.handleProfileWalletPersist = async (options = {}) => {
const { nwcUri, defaultZap, lastChecked } =
options && typeof options === "object" ? options : {};
const partial = {};
if (nwcUri !== undefined) {
partial.nwcUri = nwcUri;
}
if (defaultZap !== undefined) {
partial.defaultZap = defaultZap;
}
if (lastChecked !== undefined) {
partial.lastChecked = lastChecked;
}
if (!Object.keys(partial).length) {
return service.getActiveNwcSettings();
}
return service.updateActiveNwcSettings(partial);
};
service.hasActiveWalletConnection = () => {
const settings = ensure();
const candidate =
typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : "";
return candidate.length > 0;
};
service.validateWalletUri = (uri, { requireValue = false } = {}) => {
const value = typeof uri === "string" ? uri.trim() : "";
if (!value) {
if (requireValue) {
return {
valid: false,
sanitized: "",
message: "Enter a wallet connect URI before continuing.",
};
}
return { valid: true, sanitized: "" };
}
if (!value.toLowerCase().startsWith(NWC_URI_SCHEME)) {
return {
valid: false,
sanitized: value,
message: `Wallet URI must start with ${NWC_URI_SCHEME}.`,
};
}
return { valid: true, sanitized: value };
};
service.ensureWallet = async ({ nwcUri } = {}) => {
const settings = service.getActiveNwcSettings();
const candidate =
typeof nwcUri === "string" && nwcUri.trim()
? nwcUri.trim()
: settings.nwcUri || "";
const { valid, sanitized, message } = service.validateWalletUri(
candidate,
{ requireValue: true },
);
if (!valid) {
throw new Error(message || "Invalid wallet URI provided.");
}
return { ...settings, nwcUri: sanitized };
};
service.clearStoredNwcSettings = async () => true;
service.onLogin = async () => service.getActiveNwcSettings();
service.onLogout = async () => service.createDefaultNwcSettings();
service.onIdentityChanged = () => {
internalState.walletSettings = createInternalDefaultNwcSettings();
};
return service;
},
},
{
key: "moderationSettings",
type: "object",
description:
"Service that manages Safety & Moderation threshold overrides for the active profile.",
fallback: ({ internalState }) => {
const ensure = () => ensureInternalModerationSettings(internalState);
const sanitize = (value, fallback) => {
if (value === null || value === undefined) {
return fallback;
}
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return Math.max(0, Math.floor(numeric));
};
const service = {};
service.getDefaultModerationSettings = () =>
createInternalDefaultModerationSettings();
service.getActiveModerationSettings = () => {
const current = ensure();
const defaults = service.getDefaultModerationSettings();
return {
blurThreshold: sanitize(current.blurThreshold, defaults.blurThreshold),
autoplayBlockThreshold: sanitize(
current.autoplayBlockThreshold,
defaults.autoplayBlockThreshold,
),
};
};
service.updateModerationSettings = (partial = {}) => {
const defaults = service.getDefaultModerationSettings();
const current = ensure();
const next = { ...current };
if (Object.prototype.hasOwnProperty.call(partial, "blurThreshold")) {
const value = partial.blurThreshold;
next.blurThreshold =
value === null
? defaults.blurThreshold
: sanitize(value, defaults.blurThreshold);
}
if (
Object.prototype.hasOwnProperty.call(
partial,
"autoplayBlockThreshold",
)
) {
const value = partial.autoplayBlockThreshold;
next.autoplayBlockThreshold =
value === null
? defaults.autoplayBlockThreshold
: sanitize(value, defaults.autoplayBlockThreshold);
}
internalState.moderationSettings = next;
return service.getActiveModerationSettings();
};
service.resetModerationSettings = () => {
internalState.moderationSettings = createInternalDefaultModerationSettings();
return service.getActiveModerationSettings();
};
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",
description: "Triggers a video reload so UI reflects profile or permission changes.",
fallback: () => async () => undefined,
},
{
key: "onVideosShouldRefresh",
type: "function",
description:
"Signals that video listings should be refreshed after a profile-driven mutation.",
fallback: ({ services, resolved }) => async (context = {}) => {
const loader =
typeof services.loadVideos === "function"
? services.loadVideos
: typeof resolved.loadVideos === "function"
? resolved.loadVideos
: null;
if (loader) {
return loader(true, context);
}
return undefined;
},
},
{
key: "sendAdminListNotification",
type: "function",
description:
"Sends an administrative notification when moderators/whitelist/blacklist entries change.",
optional: true,
},
{
key: "describeAdminError",
type: "function",
description:
"Maps low-level admin errors to readable copy for surfaced error messages.",
fallback: () => () => "",
},
{
key: "describeNotificationError",
type: "function",
description:
"Maps notification send errors to human readable descriptions for toast/banner messaging.",
fallback: () => () => "",
},
{
key: "onAccessControlUpdated",
type: "function",
description:
"Callback invoked after access control mutations so the app can refresh dependent UI.",
fallback: () => async () => undefined,
},
{
key: "persistSavedProfiles",
type: "function",
description:
"Persists the saved profile collection (and optionally the active profile) to storage.",
fallback: () => () => undefined,
},
];
const STATE_CONTRACT = [
{
key: "getSavedProfiles",
type: "function",
description:
"Returns the saved profile entries that populate the account switcher UI.",
fallback: (internal) => () => internal.savedProfiles.slice(),
},
{
key: "setSavedProfiles",
type: "function",
description:
"Replaces the saved profile entries and returns the newly stored collection.",
fallback: (internal) => (profiles = []) => {
internal.savedProfiles = Array.isArray(profiles)
? profiles.slice()
: [];
return internal.savedProfiles;
},
},
{
key: "persistSavedProfiles",
type: "function",
description:
"Persists the saved profile collection (and optionally the active profile) to storage.",
fallback: () => noop,
},
{
key: "getActivePubkey",
type: "function",
description: "Returns the currently active profile pubkey.",
fallback: (internal) => () => internal.activePubkey,
},
{
key: "setActivePubkey",
type: "function",
description: "Updates the active profile pubkey and returns the stored value.",
fallback: (internal) => (pubkey) => {
if (typeof pubkey === "string") {
const trimmed = pubkey.trim();
internal.activePubkey = trimmed ? trimmed : null;
} else {
internal.activePubkey = null;
}
return internal.activePubkey;
},
},
{
key: "getCachedSelection",
type: "function",
description:
"Reads the cached profile selection used to restore switcher focus after reloads.",
fallback: (internal) => () => internal.cachedSelection,
},
{
key: "setCachedSelection",
type: "function",
description:
"Caches the last selected profile identifier and returns the stored value.",
fallback: (internal) => (value) => {
if (typeof value === "string") {
const trimmed = value.trim();
internal.cachedSelection = trimmed || null;
} else {
internal.cachedSelection = null;
}
return internal.cachedSelection;
},
},
{
key: "getActivePane",
type: "function",
description: "Returns the identifier for the currently visible profile modal pane.",
fallback: (internal) => () => internal.activePane,
},
{
key: "setActivePane",
type: "function",
description: "Updates the active pane identifier and returns the stored value.",
fallback: (internal) => (pane) => {
if (typeof pane === "string") {
const trimmed = pane.trim().toLowerCase();
internal.activePane = trimmed || "account";
} else {
internal.activePane = "account";
}
return internal.activePane;
},
},
{
key: "getWalletBusy",
type: "function",
description:
"Indicates whether wallet-related actions are currently in-flight (disables UI as needed).",
fallback: (internal) => () => internal.walletBusy,
},
{
key: "setWalletBusy",
type: "function",
description: "Updates the wallet busy flag and returns the new boolean state.",
fallback: (internal) => (isBusy) => {
internal.walletBusy = Boolean(isBusy);
return internal.walletBusy;
},
},
{
key: "getDmRecipient",
type: "function",
description:
"Returns the currently selected direct message recipient pubkey.",
fallback: (internal) => () => internal.dmRecipient,
},
{
key: "setDmRecipient",
type: "function",
description:
"Updates the selected direct message recipient pubkey and returns the stored value.",
fallback: (internal) => (pubkey) => {
if (typeof pubkey === "string") {
const trimmed = pubkey.trim();
internal.dmRecipient = trimmed || null;
} else {
internal.dmRecipient = null;
}
return internal.dmRecipient;
},
},
{
key: "getDmRelayHints",
type: "function",
description:
"Returns the cached DM relay hints for a given pubkey.",
fallback: (internal) => (pubkey) => {
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized || !(internal.dmRelayHints instanceof Map)) {
return [];
}
const hints = internal.dmRelayHints.get(normalized);
return Array.isArray(hints) ? hints.slice() : [];
},
},
{
key: "setDmRelayHints",
type: "function",
description:
"Stores DM relay hints for a given pubkey and returns the stored list.",
fallback: (internal) => (pubkey, hints = []) => {
if (!(internal.dmRelayHints instanceof Map)) {
internal.dmRelayHints = new Map();
}
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized) {
return [];
}
const stored = Array.isArray(hints) ? hints.slice() : [];
internal.dmRelayHints.set(normalized, stored);
return stored;
},
},
{
key: "getDmRelayPreferences",
type: "function",
description:
"Returns the cached DM relay hints for a given pubkey (publisher view).",
fallback: (internal) => (pubkey) => {
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized || !(internal.dmRelayHints instanceof Map)) {
return [];
}
const hints = internal.dmRelayHints.get(normalized);
return Array.isArray(hints) ? hints.slice() : [];
},
},
{
key: "setDmRelayPreferences",
type: "function",
description:
"Stores DM relay hints for a given pubkey (publisher view).",
fallback: (internal) => (pubkey, hints = []) => {
if (!(internal.dmRelayHints instanceof Map)) {
internal.dmRelayHints = new Map();
}
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized) {
return [];
}
const stored = Array.isArray(hints) ? hints.slice() : [];
internal.dmRelayHints.set(normalized, stored);
return stored;
},
},
{
key: "getDmPrivacySettings",
type: "function",
description:
"Returns the DM privacy settings (read receipts and typing indicators) for the active profile.",
fallback: (internal) => () => {
if (!(internal.dmPrivacySettings instanceof Map)) {
internal.dmPrivacySettings = new Map();
}
const settings = internal.dmPrivacySettings.get("default");
return settings ? { ...settings } : createInternalDefaultDmPrivacySettings();
},
},
{
key: "setDmPrivacySettings",
type: "function",
description:
"Updates the DM privacy settings (read receipts and typing indicators) for the active profile.",
fallback: (internal) => (settings = {}) => {
if (!(internal.dmPrivacySettings instanceof Map)) {
internal.dmPrivacySettings = new Map();
}
const base = createInternalDefaultDmPrivacySettings();
const normalized = {
readReceiptsEnabled:
typeof settings.readReceiptsEnabled === "boolean"
? settings.readReceiptsEnabled
: base.readReceiptsEnabled,
typingIndicatorsEnabled:
typeof settings.typingIndicatorsEnabled === "boolean"
? settings.typingIndicatorsEnabled
: base.typingIndicatorsEnabled,
};
internal.dmPrivacySettings.set("default", normalized);
return { ...normalized };
},
},
];
function buildServicesContract(services = {}, internalState) {
const resolved = {};
const missing = [];
SERVICE_CONTRACT.forEach((definition) => {
const provided = services[definition.key];
if (provided == null) {
if (typeof definition.fallback === "function") {
const fallbackValue = definition.fallback({
services,
resolved,
internalState,
definition,
});
if (definition.type === "function" && typeof fallbackValue === "function") {
resolved[definition.key] = fallbackValue;
return;
}
if (
definition.type === "object" &&
typeof fallbackValue === "object" &&
fallbackValue !== null
) {
resolved[definition.key] = fallbackValue;
return;
}
}
if (definition.optional) {
return;
}
missing.push(`- ${definition.key}: ${definition.description}`);
return;
}
if (definition.type === "function" && typeof provided !== "function") {
throw new TypeError(
`Expected service \"${definition.key}\" to be a function. Received ${typeof provided}.`,
);
}
if (
definition.type === "object" &&
(typeof provided !== "object" || provided === null)
) {
throw new TypeError(
`Expected service \"${definition.key}\" to be an object. Received ${typeof provided}.`,
);
}
resolved[definition.key] = provided;
});
if (missing.length) {
throw new Error(
[
"[ProfileModalController] Missing required services for profile modal controller:",
...missing,
].join("\n"),
);
}
return Object.freeze({ ...services, ...resolved });
}
function buildStateContract(state = {}, internalState) {
const resolved = {};
STATE_CONTRACT.forEach((definition) => {
const value = state[definition.key];
if (typeof value === definition.type) {
resolved[definition.key] = value;
return;
}
if (definition.fallback) {
const fallback = definition.fallback(internalState);
if (typeof fallback === definition.type) {
resolved[definition.key] = fallback;
return;
}
}
throw new Error(
`[ProfileModalController] Missing state handler \"${definition.key}\" (${definition.description}).`,
);
});
return { ...state, ...resolved };
}
export class ProfileModalController {
constructor(options = {}) {
const {
+1 -1
View File
@@ -26,7 +26,7 @@ const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much f
// These files are known to be oversized and are decomposition targets.
// They must NOT grow beyond (recorded + GROWTH_MARGIN).
const GRANDFATHERED = {
"js/ui/profileModalController.js": 14159,
"js/ui/profileModalController.js": 13275,
"js/ui/components/VideoModal.js": 6284,
"js/app.js": 5602,
"js/channelProfile.js": 5529,