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,
|
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>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user