import React, { useEffect, useState } from "react"; import { useStore } from "@nanostores/react"; import { useTranslation } from "react-i18next"; import { languages } from "virtual:i18n-languages"; import { $user, logout } from "../../store/auth"; import { $theme, setTheme, type Theme } from "../../store/theme"; import { $preferences, loadPreferences, addLabeler, removeLabeler, setLabelVisibility, getLabelVisibility, setDisableExternalLinkWarning, setEnableCommunityBookmarks, } from "../../store/preferences"; import { getAPIKeys, createAPIKey, deleteAPIKey, getBlocks, getMutes, unblockUser, unmuteUser, getLabelerInfo, type APIKey, } from "../../api/client"; import type { BlockedUser, MutedUser, LabelerInfo, LabelVisibility as LabelVisibilityType, ContentLabelValue, } from "../../types"; import { Copy, Trash2, Key, Plus, Check, Sun, Moon, Monitor, LogOut, ChevronRight, ShieldBan, VolumeX, ShieldOff, Volume2, Shield, Eye, EyeOff, XCircle, Upload, } from "lucide-react"; import { Avatar, Button, Input, Skeleton, EmptyState, Switch, } from "../../components/ui"; import { AppleIcon } from "../../components/common/Icons"; import { HighlightImporter } from "./HighlightImporter"; import IOSShortcutModal from "../../components/modals/IOSShortcutModal"; import { analytics } from "../../lib/analytics"; export default function Settings() { const { t } = useTranslation(); const user = useStore($user); const theme = useStore($theme); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [newKeyName, setNewKeyName] = useState(""); const [createdKey, setCreatedKey] = useState(null); const [justCopied, setJustCopied] = useState(false); const [creating, setCreating] = useState(false); const [blocks, setBlocks] = useState([]); const [mutes, setMutes] = useState([]); const [modLoading, setModLoading] = useState(true); const [labelerInfo, setLabelerInfo] = useState(null); const [newLabelerDid, setNewLabelerDid] = useState(""); const [addingLabeler, setAddingLabeler] = useState(false); const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false); const preferences = useStore($preferences); const { i18n: i18nInstance } = useTranslation(); const [currentLanguage, setCurrentLanguage] = useState( () => i18nInstance.resolvedLanguage ?? i18nInstance.language ?? "en", ); useEffect(() => { const handler = (lng: string) => setCurrentLanguage(lng); i18nInstance.on("languageChanged", handler); return () => { i18nInstance.off("languageChanged", handler); }; }, [i18nInstance]); useEffect(() => { const loadKeys = async () => { setLoading(true); const data = await getAPIKeys(); setKeys(data); setLoading(false); }; loadKeys(); const loadModeration = async () => { setModLoading(true); const [blocksData, mutesData] = await Promise.all([ getBlocks(), getMutes(), ]); setBlocks(blocksData); setMutes(mutesData); setModLoading(false); }; loadModeration(); loadPreferences(); getLabelerInfo().then(setLabelerInfo); }, []); const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); if (!newKeyName.trim()) return; setCreating(true); const res = await createAPIKey(newKeyName); if (res) { setKeys([res, ...keys]); setCreatedKey(res.key || null); setNewKeyName(""); analytics.capture("api_key_created"); } setCreating(false); }; const handleDelete = async (id: string) => { if (window.confirm(t("settings.apiKeys.revokeConfirm"))) { const success = await deleteAPIKey(id); if (success) { setKeys((prev) => prev.filter((k) => k.id !== id)); } } }; const copyToClipboard = async (text: string) => { await navigator.clipboard.writeText(text); setJustCopied(true); setTimeout(() => setJustCopied(false), 2000); }; if (!user) return null; const themeOptions: { value: Theme; label: string; icon: typeof Sun }[] = [ { value: "light", label: t("nav.themeLight"), icon: Sun }, { value: "dark", label: t("nav.themeDark"), icon: Moon }, { value: "system", label: t("nav.themeSystem"), icon: Monitor }, ]; return (

{t("settings.title")}

{t("settings.sections.profile")}

{user.displayName || user.handle}

@{user.handle}

{t("settings.sections.appearance")}

{themeOptions.map((opt) => ( ))}

{t("settings.appearance.disableExternalLinkWarning")}

{t("settings.appearance.disableExternalLinkWarningDesc")}

{t("settings.appearance.communityBookmarks")}

{t("settings.appearance.communityBookmarksDesc")}

{languages.length > 1 && (

{t("settings.sections.language")}

{t("settings.language.description")}

{languages.map((lang) => ( ))}
)}

{t("settings.sections.batchImport")}

{t("settings.batchImport.description")}

{t("settings.sections.apiKeys")}

{t("settings.apiKeys.description")}

setNewKeyName(e.target.value)} placeholder={t("settings.apiKeys.keyNamePlaceholder")} />
{createdKey && (

{t("settings.apiKeys.copyNow")}

{createdKey}
)} {loading ? (
) : keys.length === 0 ? ( } message={t("settings.apiKeys.empty")} /> ) : (
{keys.map((key) => (

{key.name}

{t("settings.apiKeys.created", { date: new Date(key.createdAt).toLocaleDateString(), })}

))}
)}

{t("settings.sections.moderation")}

{t("settings.moderation.description")}

{modLoading ? (
) : (

{t("settings.moderation.blockedAccounts", { count: blocks.length, })}

{blocks.length === 0 ? (

{t("settings.moderation.noBlocked")}

) : (
{blocks.map((b) => (

{b.author?.displayName || b.author?.handle || b.did}

{b.author?.handle && (

@{b.author.handle}

)}
))}
)}

{t("settings.moderation.mutedAccounts", { count: mutes.length, })}

{mutes.length === 0 ? (

{t("settings.moderation.noMuted")}

) : (
{mutes.map((m) => (

{m.author?.displayName || m.author?.handle || m.did}

{m.author?.handle && (

@{m.author.handle}

)}
))}
)}
)}

{t("settings.sections.contentFiltering")}

{t("settings.contentFiltering.description")}

{t("settings.contentFiltering.subscribedLabelers")}

{preferences.subscribedLabelers.length === 0 ? (

{t("settings.contentFiltering.noLabelers")}

) : (
{preferences.subscribedLabelers.map((labeler) => (

{labelerInfo?.did === labeler.did ? labelerInfo.name : labeler.did}

{labeler.did}

))}
)}
{ e.preventDefault(); if (!newLabelerDid.trim()) return; setAddingLabeler(true); await addLabeler(newLabelerDid.trim()); setNewLabelerDid(""); setAddingLabeler(false); }} className="flex gap-2" >
setNewLabelerDid(e.target.value)} placeholder={t( "settings.contentFiltering.labelerDidPlaceholder", )} />
{preferences.subscribedLabelers.length > 0 && (

{t("settings.contentFiltering.labelVisibility")}

{t("settings.contentFiltering.labelVisibilityDesc")}

{preferences.subscribedLabelers.map((labeler) => { const labels: ContentLabelValue[] = [ "sexual", "nudity", "violence", "gore", "spam", "misleading", ]; return (

{labelerInfo?.did === labeler.did ? labelerInfo.name : labeler.did}

{labels.map((label) => { const current = getLabelVisibility( labeler.did, label, ); const options: { value: LabelVisibilityType; label: string; icon: typeof Eye; }[] = [ { value: "warn", label: t("settings.contentFiltering.warn"), icon: EyeOff, }, { value: "hide", label: t("settings.contentFiltering.hide"), icon: XCircle, }, { value: "ignore", label: t("settings.contentFiltering.ignore"), icon: Eye, }, ]; return (
{t(`card.labelDescriptions.${label}`)}
{options.map((opt) => ( ))}
); })}
); })}
)}

{t("settings.sections.iosShortcut")}

{t("settings.iosShortcut.description")}

setIsShortcutModalOpen(false)} />
); }