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,
Users,
Keyboard,
Loader2,
} from 'lucide-react';
import {
SidebarData,
@@ -237,6 +238,7 @@ export default function EvercloneWorkspace() {
const [totalNotes, setTotalNotes] = useState<number>(0);
const [loadingList, setLoadingList] = useState<boolean>(true);
const [loadingMore, setLoadingMore] = useState<boolean>(false);
const loadingMoreRef = useRef<boolean>(false);
const [hasMoreNotes, setHasMoreNotes] = useState<boolean>(true);
const [selectedNoteId, setSelectedNoteId] = useState<string>('');
const [activeNote, setActiveNote] = useState<NoteDetail | null>(null);
@@ -441,7 +443,8 @@ export default function EvercloneWorkspace() {
);
const fetchMoreNotes = useCallback(async () => {
if (loadingMore || !hasMoreNotes || loadingList) return;
if (loadingMoreRef.current || !hasMoreNotes || loadingList) return;
loadingMoreRef.current = true;
const currentReqId = listRequestIdRef.current;
setLoadingMore(true);
try {
@@ -468,11 +471,12 @@ export default function EvercloneWorkspace() {
console.error('Failed to fetch next page of notes:', e);
}
} finally {
loadingMoreRef.current = false;
if (currentReqId === listRequestIdRef.current) {
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.
const displayedNotes = useMemo(() => {
@@ -1306,25 +1310,19 @@ export default function EvercloneWorkspace() {
}
}, [listMode, rowVirtualizer]);
// Infinite scroll trigger: when scrolling near bottom, fetch next page of notes
const virtualItems = rowVirtualizer.getVirtualItems();
useEffect(() => {
if (!virtualItems.length || !hasMoreNotes || loadingMore || loadingList) return;
const lastItem = virtualItems[virtualItems.length - 1];
const totalVirtual = listMode === 'cards' ? cardGridRowCount : displayedNotes.length;
if (lastItem && lastItem.index >= totalVirtual - 6) {
fetchMoreNotes();
}
}, [
virtualItems,
hasMoreNotes,
loadingMore,
loadingList,
listMode,
cardGridRowCount,
displayedNotes.length,
fetchMoreNotes,
]);
// User-scroll driven infinite pagination: only triggers when user deliberately scrolls near bottom
const handleListScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget;
const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
if (distanceToBottom < 300) {
if (!loadingMoreRef.current && hasMoreNotes && !loadingList) {
fetchMoreNotes();
}
}
},
[hasMoreNotes, loadingList, fetchMoreNotes]
);
// Compute current view title
const currentViewTitle = (() => {
@@ -2267,9 +2265,16 @@ export default function EvercloneWorkspace() {
</div>
<div className="flex items-center justify-between gap-1.5 text-xs text-gray-500">
<span className="truncate text-[11px]">
{totalNotes} {totalNotes === 1 ? 'note' : 'notes'}
{debouncedSearch ? ` · "${debouncedSearch}"` : ''}
<span className="truncate text-[11px] flex items-center gap-1.5">
<span>
{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>
<div className="flex items-center gap-1 sm:gap-1.5 shrink-0">
@@ -2350,16 +2355,6 @@ export default function EvercloneWorkspace() {
</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 */}
{showFilterPanel && (
@@ -2719,7 +2714,7 @@ export default function EvercloneWorkspace() {
</div>
{/* 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 ? (
<div className="p-8 text-center text-xs text-gray-400">Loading notes…</div>
) : displayedNotes.length === 0 ? (
@@ -3133,6 +3128,12 @@ export default function EvercloneWorkspace() {
})}
</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>
</section>