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 MutualFollowersModalProps { actor: string; onClose: () => void; } interface FollowProfile { did: string; handle: string; displayName?: string; avatar?: string; followsYou?: boolean; } export default function MutualFollowersModal({ actor, onClose }: MutualFollowersModalProps) { 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]); // Fetch all following of the logged-in user, then filter followers to mutuals useEffect(() => { let cancelled = false; (async () => { try { // 1. Get the logged-in user's following list (all of them) const myFollowingDids = new Set(); let followingCursor: string | undefined; do { const resp = await atprotoClient.getFollows( atprotoClient.api!.did!, 100, followingCursor, ); for (const f of resp.follows) { if (f.did) myFollowingDids.add(f.did); } followingCursor = resp.cursor; } while (followingCursor && !cancelled); if (cancelled) return; // 2. Get the target's followers const resp = await atprotoClient.getFollowers(actor); if (!cancelled) { // Filter to only mutual followers (people you follow) const mutuals = (resp.followers as FollowProfile[]).filter( (f) => myFollowingDids.has(f.did), ); setProfiles(mutuals); // We don't paginate the filtered list — load more fetches next page and filters 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]); const loadMore = useCallback(async () => { if (!cursor || loadingMore) return; setLoadingMore(true); try { // Re-fetch the logged-in user's following (cached by the API client usually) const myFollowingDids = new Set(); let followingCursor: string | undefined; do { const resp = await atprotoClient.getFollows( atprotoClient.api!.did!, 100, followingCursor, ); for (const f of resp.follows) { if (f.did) myFollowingDids.add(f.did); } followingCursor = resp.cursor; } while (followingCursor); const resp = await atprotoClient.getFollowers(actor, 50, cursor); const mutuals = (resp.followers as FollowProfile[]).filter( (f) => myFollowingDids.has(f.did), ); setProfiles((prev) => [...prev, ...mutuals]); setCursor(resp.cursor); } catch (err) { console.error('Failed to load more:', err); } finally { setLoadingMore(false); } }, [actor, cursor, loadingMore]); 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()}>
Followers you know
{loading &&
Loading…
} {error &&
{error}
} {!loading && !error && profiles.length === 0 && (
No followers you know yet
)} {profiles.map((p) => ( ))} {cursor && ( )}
); }