Something went wrong. Try again.
A Bsky-like frontend using the atprotocol natively.
Something went wrong. Try again.
4.6 kB · 121 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122import { useState, useEffect, useCallback } from 'react';import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';import { faXmark } from '@fortawesome/free-solid-svg-icons';import type { AtUri } from '../api/types';import { atprotoClient } from '../api/client';import { usePostLabels } from '../settings/usePostLabels';import PdsFavicon from './PdsFavicon';import { useMuteBlock } from './muteBlockContext';import './LikesRepostsModal.css';
interface RepostsModalProps { uri: AtUri; onClose: () => void;}
interface RepostActor { did: string; handle: string; displayName?: string; avatar?: string;}
export default function RepostsModal({ uri, onClose }: RepostsModalProps) { const [actors, setActors] = useState<RepostActor[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [cursor, setCursor] = useState<string | undefined>(); const [loadingMore, setLoadingMore] = useState(false); const { t: label } = usePostLabels(); 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 { const resp = await atprotoClient.api!.app.bsky.feed.getRepostedBy({ uri, limit: 50 }); if (!cancelled) { const repostedBy = (resp.data as { repostedBy: RepostActor[] }).repostedBy; setActors(repostedBy); setCursor((resp.data as { cursor?: string }).cursor); } } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load reposts'); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [uri]);
const loadMore = useCallback(async () => { if (!cursor || loadingMore) return; setLoadingMore(true); try { const resp = await atprotoClient.api!.app.bsky.feed.getRepostedBy({ uri, limit: 50, cursor }); const repostedBy = (resp.data as { repostedBy: RepostActor[] }).repostedBy; setActors((prev) => [...prev, ...repostedBy]); setCursor((resp.data as { cursor?: string }).cursor); } catch (err) { console.error('Failed to load more reposts:', err); } finally { setLoadingMore(false); } }, [uri, cursor, loadingMore]);
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">{label('repost', false, true)}ed by</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 && actors.length === 0 && ( <div className="lr-modal-empty">No {label('repost', true)} yet</div> )} {actors.map((actor) => ( <div key={actor.did} className="lr-modal-user"> {actor.avatar ? ( <img className="lr-modal-user-avatar" src={actor.avatar} alt="" /> ) : ( <div className="lr-modal-user-avatar-placeholder"> {(actor.displayName || actor.handle)[0].toUpperCase()} </div> )} <div className="lr-modal-user-info"> <span className="lr-modal-user-name">{actor.displayName || actor.handle}</span> <PdsFavicon did={actor.did} /> <span className="lr-modal-user-handle">@{actor.handle}</span> {isBlocked(actor.did) && <span className="blocked-badge">Blocked</span>} {isMuted(actor.did) && <span className="muted-badge">Muted</span>} </div> </div> ))} {cursor && ( <button className="lr-modal-load-more" onClick={loadMore} disabled={loadingMore}> {loadingMore ? 'Loading…' : 'Load more'} </button> )} </div> </div> </div> );}