Something went wrong. Try again.
A Bsky-like frontend using the atprotocol natively.
Something went wrong. Try again.
6.0 kB · 161 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162import { 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<FollowProfile[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [cursor, setCursor] = useState<string | undefined>(); 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 ( <div className="lr-modal-overlay" onClick={onClose}> <div className="lr-modal-content" onClick={(e) => e.stopPropagation()}> <div className="lr-modal-header"> <span className="lr-modal-title"> {mode === 'followers' ? 'Followers' : 'Following'} </span> <button className="lr-modal-close" onClick={onClose}> <FontAwesomeIcon icon={faXmark} /> </button> </div> <div className="lr-modal-body"> {loading && <div className="lr-modal-loading">Loading…</div>} {error && <div className="lr-modal-error">{error}</div>} {!loading && !error && profiles.length === 0 && ( <div className="lr-modal-empty"> {mode === 'followers' ? 'No followers yet' : 'Not following anyone yet'} </div> )} {profiles.map((p) => ( <button key={p.did} className="lr-modal-user" onClick={() => handleProfileClick(p)} onAuxClick={(e) => { if (e.button === 1) { e.preventDefault(); window.open(`/profile/${encodeURIComponent(p.handle)}`, '_blank'); } }} style={{ cursor: 'pointer', textAlign: 'left', width: '100%', background: 'none', border: 'none', padding: '10px 16px', display: 'flex', alignItems: 'center', gap: '12px' }} > {p.avatar ? ( <img className="lr-modal-user-avatar" src={p.avatar} alt="" /> ) : ( <div className="lr-modal-user-avatar-placeholder"> {(p.displayName || p.handle)[0].toUpperCase()} </div> )} <div className="lr-modal-user-info"> <span className="lr-modal-user-name">{p.displayName || p.handle}</span> <PdsFavicon did={p.did} /> <span className="lr-modal-user-handle">@{p.handle}</span> {isBlocked(p.did) && <span className="blocked-badge">Blocked</span>} {isMuted(p.did) && <span className="muted-badge">Muted</span>} </div> </button> ))} {cursor && ( <button className="lr-modal-load-more" onClick={loadMore} disabled={loadingMore}> {loadingMore ? 'Loading…' : 'Load more'} </button> )} </div> </div> </div> );}