import React, { useEffect, useState } from "react"; import { useStore } from "@nanostores/react"; import { $user, logout } from "../../store/auth"; import { $theme, setTheme, type Theme } from "../../store/theme"; import { $preferences, loadPreferences, addLabeler, removeLabeler, setLabelVisibility, getLabelVisibility, setDisableExternalLinkWarning, } 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 { Link } from "react-router-dom"; import { HighlightImporter } from "./HighlightImporter"; import IOSShortcutModal from "../../components/modals/IOSShortcutModal"; export default function Settings() { 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); 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(""); } setCreating(false); }; const handleDelete = async (id: string) => { if (window.confirm("Revoke this key? Apps using it will stop working.")) { 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: "Light", icon: Sun }, { value: "dark", label: "Dark", icon: Moon }, { value: "system", label: "System", icon: Monitor }, ]; return (

Settings

Profile

{user.displayName || user.handle}

@{user.handle}

Appearance

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

Disable external link warning

Don't ask for confirmation when opening external links

Batch Import Highlights

Upload highlights from CSV. Required: url, text. Optional: title, tags, color, created_at

API Keys

For the iOS shortcut and other apps

setNewKeyName(e.target.value)} placeholder="Key name, e.g. iOS Shortcut" />
{createdKey && (

Copy now - you won't see this again!

{createdKey}
)} {loading ? (
) : keys.length === 0 ? ( } message="No API keys yet. Create one to use with the browser extension." /> ) : (
{keys.map((key) => (

{key.name}

Created {new Date(key.createdAt).toLocaleDateString()}

))}
)}

Moderation

Manage blocked and muted accounts

{modLoading ? (
) : (

Blocked accounts ({blocks.length})

{blocks.length === 0 ? (

No blocked accounts

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

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

{b.author?.handle && (

@{b.author.handle}

)}
))}
)}

Muted accounts ({mutes.length})

{mutes.length === 0 ? (

No muted accounts

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

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

{m.author?.handle && (

@{m.author.handle}

)}
))}
)}
)}

Content Filtering

Subscribe to labelers and configure how labeled content appears

Subscribed Labelers

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

No labelers subscribed

) : (
{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="did:plc:... (labeler DID)" />
{preferences.subscribedLabelers.length > 0 && (

Label Visibility

Choose how to handle each label type: Warn{" "} shows a blur overlay, Hide removes content entirely, Ignore shows content normally.

{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: "Warn", icon: EyeOff }, { value: "hide", label: "Hide", icon: XCircle }, { value: "ignore", label: "Ignore", icon: Eye }, ]; return (
{label}
{options.map((opt) => ( ))}
); })}
); })}
)}

iOS Shortcut

Save pages to Margin from Safari on iPhone and iPad

setIsShortcutModalOpen(false)} />
); }