Fix infinite scroll loop and add explicit note loaded count
This commit is contained in:
1 parent
ad640397e6
commit
0f24af880b
1 file changed
+36
-35
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user