import { useState, useEffect, useCallback } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { atprotoClient } from '../api/client'; import { useProfileViewMode } from '../settings/useProfileViewMode'; import { useProfileViewer } from './useProfileViewer'; import PdsFavicon from './PdsFavicon'; import { useNavigate } from 'react-router-dom'; import { useMuteBlock } from './muteBlockContext'; import './LikesRepostsModal.css'; interface FollowListModalProps { actor: string; mode: 'followers' | 'following'; onClose: () => void; } interface FollowProfile { did: string; handle: string; displayName?: string; avatar?: string; } export default function FollowListModal({ actor, mode, onClose }: FollowListModalProps) { const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [cursor, setCursor] = useState(); const [loadingMore, setLoadingMore] = useState(false); const { mode: profileViewMode } = useProfileViewMode(); const { open: openProfile } = useProfileViewer(); const navigate = useNavigate(); const { isBlocked, isMuted } = useMuteBlock(); useEffect(() => { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }, []); useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [onClose]); useEffect(() => { let cancelled = false; (async () => { try { if (mode === 'followers') { const resp = await atprotoClient.getFollowers(actor); if (!cancelled) { setProfiles(resp.followers as FollowProfile[]); setCursor(resp.cursor); } } else { const resp = await atprotoClient.getFollows(actor); if (!cancelled) { setProfiles(resp.follows as FollowProfile[]); setCursor(resp.cursor); } } } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load'); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [actor, mode]); const loadMore = useCallback(async () => { if (!cursor || loadingMore) return; setLoadingMore(true); try { if (mode === 'followers') { const resp = await atprotoClient.getFollowers(actor, 50, cursor); setProfiles((prev) => [...prev, ...(resp.followers as FollowProfile[])]); setCursor(resp.cursor); } else { const resp = await atprotoClient.getFollows(actor, 50, cursor); setProfiles((prev) => [...prev, ...(resp.follows as FollowProfile[])]); setCursor(resp.cursor); } } catch (err) { console.error('Failed to load more:', err); } finally { setLoadingMore(false); } }, [actor, cursor, loadingMore, mode]); const handleProfileClick = useCallback((p: FollowProfile) => { onClose(); const profileRoute = `/profile/${encodeURIComponent(p.handle)}`; switch (profileViewMode) { case 'page': navigate(profileRoute); break; case 'side': window.history.pushState(null, '', profileRoute); openProfile(p.handle); break; } }, [onClose, profileViewMode, navigate, openProfile]); return (
e.stopPropagation()}>
{mode === 'followers' ? 'Followers' : 'Following'}
{loading &&
Loading…
} {error &&
{error}
} {!loading && !error && profiles.length === 0 && (
{mode === 'followers' ? 'No followers yet' : 'Not following anyone yet'}
)} {profiles.map((p) => ( ))} {cursor && ( )}
); }