diff --git a/.gitignore b/.gitignore index a709d5ad..00c35f77 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,7 @@ dmypy.json # Node.js (apps/frontend) # =========================== node_modules +apps/frontend/node_modules .npm .yarn/ .pnp.* diff --git a/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx b/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx index 36370723..b21b877c 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useCallback } from 'react'; +import { useRef, useEffect, useCallback, useState } from 'react'; import { Loader2, AlertCircle } from 'lucide-react'; import { ScrollArea } from '../../ui/scroll-area'; import { IssueListItem } from './IssueListItem'; @@ -19,6 +19,7 @@ export function IssueList({ }: IssueListProps) { const { t } = useTranslation('common'); const loadMoreTriggerRef = useRef(null); + const [viewportElement, setViewportElement] = useState(null); // Intersection Observer for infinite scroll const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => { @@ -30,10 +31,10 @@ export function IssueList({ useEffect(() => { const trigger = loadMoreTriggerRef.current; - if (!trigger || !onLoadMore) return; + if (!trigger || !onLoadMore || !viewportElement) return; const observer = new IntersectionObserver(handleIntersection, { - root: null, + root: viewportElement, rootMargin: '100px', threshold: 0 }); @@ -43,7 +44,7 @@ export function IssueList({ return () => { observer.disconnect(); }; - }, [handleIntersection, onLoadMore]); + }, [handleIntersection, onLoadMore, viewportElement]); // Only show blocking error view when no issues are loaded // Load-more errors are shown inline near the load-more trigger @@ -71,7 +72,7 @@ export function IssueList({ } return ( - +
{issues.map((issue) => ( (null); const loadMoreTriggerRef = useRef(null); + const [viewportElement, setViewportElement] = useState(null); // Intersection Observer for infinite scroll const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => { @@ -220,11 +220,11 @@ export function PRList({ useEffect(() => { const trigger = loadMoreTriggerRef.current; - if (!trigger) return; + if (!trigger || !viewportElement) return; const observer = new IntersectionObserver(handleIntersection, { - root: null, // Use viewport as root - rootMargin: '100px', // Start loading 100px before reaching the bottom + root: viewportElement, + rootMargin: '100px', threshold: 0 }); @@ -233,7 +233,7 @@ export function PRList({ return () => { observer.disconnect(); }; - }, [handleIntersection]); + }, [handleIntersection, onLoadMore, viewportElement]); if (isLoading && prs.length === 0) { return ( @@ -268,7 +268,7 @@ export function PRList({ } return ( - +
{prs.map((pr) => { const reviewState = getReviewStateForPR(pr.number); diff --git a/apps/frontend/src/renderer/components/ui/scroll-area.tsx b/apps/frontend/src/renderer/components/ui/scroll-area.tsx index 2ca201e7..df191f98 100644 --- a/apps/frontend/src/renderer/components/ui/scroll-area.tsx +++ b/apps/frontend/src/renderer/components/ui/scroll-area.tsx @@ -5,23 +5,34 @@ import { cn } from '../../lib/utils'; const ScrollArea = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - viewportClassName?: string; -} ->(({ className, children, viewportClassName, ...props }, ref) => ( - - void; + } +>(({ className, children, viewportClassName, onViewportRef, ...props }, ref) => { + const viewportRef = React.useCallback( + (element: HTMLDivElement | null) => { + onViewportRef?.(element); + }, + [onViewportRef] + ); + + return ( + - {children} - - - - -)); + + {children} + + + + + ); +}); ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; const ScrollBar = React.forwardRef<