import { useState, type ReactNode } from 'react';
import {
Users, Download, Loader2, X, Pencil, Home, Globe, MapPin,
Trash2, Plus, UserX, Hash, MessageSquareOff, ExternalLink,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { useToast } from '@/hooks/useToast';
import { useSavedFeeds } from '@/hooks/useSavedFeeds';
import { useInterests } from '@/hooks/useInterests';
import { useFeedSettings } from '@/hooks/useFeedSettings';
import { useEncryptedSettings } from '@/hooks/useEncryptedSettings';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { useMuteList, type MuteListItem } from '@/hooks/useMuteList';
import { useAuthor } from '@/hooks/useAuthor';
import { FeedEditModal } from '@/components/FeedEditModal';
import { buildKindOptions } from '@/lib/feedFilterUtils';
import { genUserName } from '@/lib/genUserName';
import { EXTRA_KINDS } from '@/lib/extraKinds';
import { SIDEBAR_ITEMS } from '@/lib/sidebarItems';
import type { FeedSettings, SavedFeed, TabFilter, ContentWarningPolicy } from '@/contexts/AppContext';
import type { ExtraKindDef } from '@/lib/extraKinds';
export function ContentSettings() {
return (
);
}
function Section({ title, children }: { title: string; children: ReactNode }) {
return (
{title}
{children}
);
}
function FlatContentList() {
// Flat, ordered list of curated kinds. No section grouping, no sub-rows, no kind badges.
const orderedIds = [
'posts', 'replies', 'reposts', 'articles', 'highlights',
'photos', 'videos', 'voice',
'events', 'polls', 'communities', 'badges',
'reactions', 'zaps',
];
const byId = new Map(EXTRA_KINDS.map((def) => [def.id, def]));
// Replies is id 'comments' in the registry; alias here for readability.
byId.set('replies', byId.get('comments')!);
const rows = orderedIds.map((id) => byId.get(id)).filter((d): d is ExtraKindDef => !!d && !!d.agora);
return (
{rows.map((def) => (
))}
);
}
function ContentTypeRow({ def }: { def: ExtraKindDef }) {
const { feedSettings, updateFeedSettings } = useFeedSettings();
const { updateSettings } = useEncryptedSettings();
const { user } = useCurrentUser();
// Toggle key: prefer the feed inclusion key; fall back to the sidebar visibility key
// for kinds that have no direct feed key of their own (e.g. parent kinds with sub-kinds).
const toggleKey: keyof FeedSettings | undefined = def.feedKey ?? def.showKey;
if (!toggleKey) return null;
const checked = feedSettings[toggleKey] !== false;
const handleToggle = async (value: boolean) => {
const next: Partial = { [toggleKey]: value };
// Parent kinds with sub-kinds: toggle all sub-kind feed keys together so the
// single parent switch governs everything below it.
if (def.subKinds) {
for (const sub of def.subKinds) {
next[sub.feedKey] = value;
}
}
updateFeedSettings(next);
if (user) {
await updateSettings.mutateAsync({ feedSettings: { ...feedSettings, ...next } });
}
};
return (
{def.label}
{def.description}
);
}
// Feed Tabs Section Component
function FeedTabsSection() {
const { toast } = useToast();
const { updateSettings } = useEncryptedSettings();
const { user } = useCurrentUser();
const { feedSettings, updateFeedSettings } = useFeedSettings();
const [communityDomain, setCommunityDomain] = useState('');
const [isDownloading, setIsDownloading] = useState(false);
const [community, setCommunity] = useState<{ domain: string; userCount: number; label: string } | null>(() => {
const stored = localStorage.getItem('ditto:community');
return stored ? JSON.parse(stored) : null;
});
const [showAgoraFeed, setShowAgoraFeed] = useState(() => {
const stored = localStorage.getItem('agora:showWorldFeed');
return stored !== null ? stored === 'true' : true; // Default to true
});
const [showGlobalFeed, setShowGlobalFeed] = useState(() => {
const stored = localStorage.getItem('ditto:showGlobalFeed');
return stored !== null ? stored === 'true' : false; // Default to false
});
const [showCommunityFeed, setShowCommunityFeed] = useState(() => {
const stored = localStorage.getItem('ditto:showCommunityFeed');
return stored !== null ? stored === 'true' : false; // Default to false
});
const handleToggleAgoraFeed = async (checked: boolean) => {
setShowAgoraFeed(checked);
localStorage.setItem('agora:showWorldFeed', String(checked));
toast({
title: checked ? 'World feed enabled' : 'World feed disabled',
description: checked
? 'The World feed tab will appear in your navigation'
: 'The World feed tab will be hidden',
});
};
const handleToggleGlobalFeed = async (checked: boolean) => {
setShowGlobalFeed(checked);
localStorage.setItem('ditto:showGlobalFeed', String(checked));
if (user) {
await updateSettings.mutateAsync({ showGlobalFeed: checked });
}
toast({
title: checked ? 'Global feed enabled' : 'Global feed disabled',
description: checked
? 'The Global feed tab will appear in your navigation'
: 'The Global feed tab will be hidden',
});
};
const handleToggleCommunityFeed = async (checked: boolean) => {
setShowCommunityFeed(checked);
localStorage.setItem('ditto:showCommunityFeed', String(checked));
if (user) {
await updateSettings.mutateAsync({ showCommunityFeed: checked });
}
toast({
title: checked ? 'Community feed enabled' : 'Community feed disabled',
description: checked
? 'The Community feed tab will appear in your navigation'
: 'The Community feed tab will be hidden',
});
};
const handleDownloadCommunity = async () => {
if (!communityDomain.trim()) {
toast({
title: 'Domain required',
description: 'Please enter a domain name',
variant: 'destructive',
});
return;
}
// Clean up domain input
let domain = communityDomain.trim().toLowerCase();
// Remove protocol if present
domain = domain.replace(/^https?:\/\//, '');
// Remove trailing slash
domain = domain.replace(/\/$/, '');
setIsDownloading(true);
try {
// Fetch the NIP-05 JSON
const response = await fetch(`https://${domain}/.well-known/nostr.json`);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
}
const data = await response.json();
if (!data.names || typeof data.names !== 'object') {
throw new Error('Invalid NIP-05 JSON format');
}
const userCount = Object.keys(data.names).length;
// Extract label from domain (hostname without TLD)
// ditto.pub -> Ditto, spinster.xyz -> Spinster, etc.
const domainParts = domain.split('.');
const hostname = domainParts[0]; // Get first part
const label = hostname.charAt(0).toUpperCase() + hostname.slice(1); // Capitalize
// Store in localStorage (single community only)
const newCommunity = { domain, userCount, label };
setCommunity(newCommunity);
localStorage.setItem('ditto:community', JSON.stringify(newCommunity));
// Store the actual JSON data for later use
localStorage.setItem('ditto:communityData', JSON.stringify(data));
// Auto-enable the Community feed tab
setShowCommunityFeed(true);
localStorage.setItem('ditto:showCommunityFeed', 'true');
// Sync to encrypted settings
if (user) {
await updateSettings.mutateAsync({
communityData: { domain, label, userCount, nip05: data.names },
showCommunityFeed: true,
});
}
toast({
title: 'Community set',
description: `${label} with ${userCount} users`,
});
setCommunityDomain('');
} catch (error) {
console.error('Failed to download community:', error);
toast({
title: 'Failed to download',
description: error instanceof Error ? error.message : 'Could not fetch community data',
variant: 'destructive',
});
} finally {
setIsDownloading(false);
}
};
const handleRemoveCommunity = async () => {
setCommunity(null);
localStorage.removeItem('ditto:community');
localStorage.removeItem('ditto:communityData');
// Also disable the community feed tab
setShowCommunityFeed(false);
localStorage.setItem('ditto:showCommunityFeed', 'false');
if (user) {
await updateSettings.mutateAsync({ communityData: undefined, showCommunityFeed: false });
}
toast({
title: 'Community removed',
description: 'Community feed cleared',
});
};
return (
{/* Intro section for Feed Tabs */}
Feed Navigation
Manage which feed tabs appear in your navigation and follow communities by domain.
{/* Feed Tab Toggles */}
Include replies from people you follow, not just top-level posts
{
updateFeedSettings({ followsFeedShowReplies: checked });
if (user) {
await updateSettings.mutateAsync({ feedSettings: { ...feedSettings, followsFeedShowReplies: checked } });
}
toast({
title: checked ? 'Replies shown' : 'Replies hidden',
description: checked
? 'Replies from people you follow will appear in your feed'
: 'Only top-level posts will appear in your follows feed',
});
}}
className="shrink-0"
/>
Show posts from all countries around the world
Show posts from all users across the network
{community
? `Show "${community.label}" tab for ${community.domain} users`
: 'Set a community below to enable this feed'}
{/* Community Management */}
Set a community domain. We'll download the NIP-05 user list to show posts only from verified members.