Fix infinite scroll loop and add explicit note loaded count

This commit is contained in:
chrisf committed 2026-10-25 10:40:24 -06:00
1 parent ad640397e6
commit 0f24af880b
1 file changed
+36 -35
+36 -35
View File
@@ -48,6 +48,7 @@ import {
History, History,
Users, Users,
Keyboard, Keyboard,
Loader2,
} from 'lucide-react'; } from 'lucide-react';
import { import {
SidebarData, SidebarData,
@@ -237,6 +238,7 @@ export default function EvercloneWorkspace() {
const [totalNotes, setTotalNotes] = useState<number>(0); const [totalNotes, setTotalNotes] = useState<number>(0);
const [loadingList, setLoadingList] = useState<boolean>(true); const [loadingList, setLoadingList] = useState<boolean>(true);
const [loadingMore, setLoadingMore] = useState<boolean>(false); const [loadingMore, setLoadingMore] = useState<boolean>(false);
const loadingMoreRef = useRef<boolean>(false);
const [hasMoreNotes, setHasMoreNotes] = useState<boolean>(true); const [hasMoreNotes, setHasMoreNotes] = useState<boolean>(true);
const [selectedNoteId, setSelectedNoteId] = useState<string>(''); const [selectedNoteId, setSelectedNoteId] = useState<string>('');
const [activeNote, setActiveNote] = useState<NoteDetail | null>(null); const [activeNote, setActiveNote] = useState<NoteDetail | null>(null);
@@ -441,7 +443,8 @@ export default function EvercloneWorkspace() {
); );
const fetchMoreNotes = useCallback(async () => { const fetchMoreNotes = useCallback(async () => {
if (loadingMore || !hasMoreNotes || loadingList) return; if (loadingMoreRef.current || !hasMoreNotes || loadingList) return;
loadingMoreRef.current = true;
const currentReqId = listRequestIdRef.current; const currentReqId = listRequestIdRef.current;
setLoadingMore(true); setLoadingMore(true);
try { try {
@@ -468,11 +471,12 @@ export default function EvercloneWorkspace() {
console.error('Failed to fetch next page of notes:', e); console.error('Failed to fetch next page of notes:', e);
} }
} finally { } finally {
loadingMoreRef.current = false;
if (currentReqId === listRequestIdRef.current) { if (currentReqId === listRequestIdRef.current) {
setLoadingMore(false); setLoadingMore(false);
} }
} }
}, [loadingMore, hasMoreNotes, loadingList, buildNoteQueryParams, notes.length]); }, [hasMoreNotes, loadingList, buildNoteQueryParams, notes.length]);
// Safety filter: In Trash view, only trashed notes appear. In all other views, trashed notes never appear. // Safety filter: In Trash view, only trashed notes appear. In all other views, trashed notes never appear.
const displayedNotes = useMemo(() => { const displayedNotes = useMemo(() => {
@@ -1306,25 +1310,19 @@ export default function EvercloneWorkspace() {
} }
}, [listMode, rowVirtualizer]); }, [listMode, rowVirtualizer]);
// Infinite scroll trigger: when scrolling near bottom, fetch next page of notes // User-scroll driven infinite pagination: only triggers when user deliberately scrolls near bottom
const virtualItems = rowVirtualizer.getVirtualItems(); const handleListScroll = useCallback(
useEffect(() => { (e: React.UIEvent<HTMLDivElement>) => {
if (!virtualItems.length || !hasMoreNotes || loadingMore || loadingList) return; const el = e.currentTarget;
const lastItem = virtualItems[virtualItems.length - 1]; const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
const totalVirtual = listMode === 'cards' ? cardGridRowCount : displayedNotes.length; if (distanceToBottom < 300) {
if (lastItem && lastItem.index >= totalVirtual - 6) { if (!loadingMoreRef.current && hasMoreNotes && !loadingList) {
fetchMoreNotes(); fetchMoreNotes();
} }
}, [ }
virtualItems, },
hasMoreNotes, [hasMoreNotes, loadingList, fetchMoreNotes]
loadingMore, );
loadingList,
listMode,
cardGridRowCount,
displayedNotes.length,
fetchMoreNotes,
]);
// Compute current view title // Compute current view title
const currentViewTitle = (() => { const currentViewTitle = (() => {
@@ -2267,9 +2265,16 @@ export default function EvercloneWorkspace() {
</div> </div>
<div className="flex items-center justify-between gap-1.5 text-xs text-gray-500"> <div className="flex items-center justify-between gap-1.5 text-xs text-gray-500">
<span className="truncate text-[11px]"> <span className="truncate text-[11px] flex items-center gap-1.5">
{totalNotes} {totalNotes === 1 ? 'note' : 'notes'} <span>
{debouncedSearch ? ` · "${debouncedSearch}"` : ''} {displayedNotes.length < totalNotes
? `${displayedNotes.length} of ${totalNotes} notes`
: `${totalNotes} ${totalNotes === 1 ? 'note' : 'notes'}`}
{debouncedSearch ? ` · "${debouncedSearch}"` : ''}
</span>
{loadingMore && (
<Loader2 className="w-3 h-3 animate-spin text-evernote-green shrink-0" />
)}
</span> </span>
<div className="flex items-center gap-1 sm:gap-1.5 shrink-0"> <div className="flex items-center gap-1 sm:gap-1.5 shrink-0">
@@ -2350,16 +2355,6 @@ export default function EvercloneWorkspace() {
</div> </div>
</div> </div>
{/* Reserved slot for loading indicator to prevent layout bounce when loading more notes */}
<div className="h-5 flex items-center justify-center">
<div
className={`w-full py-0.5 px-3 bg-emerald-50 text-center text-[10px] text-evernote-green font-medium rounded transition-opacity duration-150 ${
loadingMore ? 'opacity-100 animate-pulse' : 'opacity-0 pointer-events-none'
}`}
>
Loading more notes…
</div>
</div>
{/* Collapsible Filter Panel */} {/* Collapsible Filter Panel */}
{showFilterPanel && ( {showFilterPanel && (
@@ -2719,7 +2714,7 @@ export default function EvercloneWorkspace() {
</div> </div>
{/* Note List Body: Card Grid vs Virtualized Snippet / Compact List */} {/* Note List Body: Card Grid vs Virtualized Snippet / Compact List */}
<div ref={listParentRef} className="flex-1 overflow-y-scroll"> <div ref={listParentRef} onScroll={handleListScroll} className="flex-1 overflow-y-scroll">
{loadingList && displayedNotes.length === 0 ? ( {loadingList && displayedNotes.length === 0 ? (
<div className="p-8 text-center text-xs text-gray-400">Loading notes…</div> <div className="p-8 text-center text-xs text-gray-400">Loading notes…</div>
) : displayedNotes.length === 0 ? ( ) : displayedNotes.length === 0 ? (
@@ -3133,6 +3128,12 @@ export default function EvercloneWorkspace() {
})} })}
</div> </div>
)} )}
{loadingMore && (
<div className="py-2.5 px-4 bg-gray-50 border-t border-gray-200/80 flex items-center justify-center gap-2 text-xs text-gray-500">
<Loader2 className="w-3.5 h-3.5 animate-spin text-evernote-green" />
<span className="text-[11px] font-medium text-gray-600">Loading more notes…</span>
</div>
)}
</div> </div>
</section> </section>