From 87a4f891a0127520665e13d51b909d004e076cd2 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 12 Mar 2025 09:26:43 +0200 Subject: [PATCH] added last item seen in virtualized list --- package-lock.json | 15 ++ package.json | 1 + src/common/VirtualizedList.tsx | 200 +++++++++++++----- .../ResourceList/ResourceListDisplay.tsx | 4 +- 4 files changed, 161 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ea2e3a..51b2e40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@mui/material": "^6.4.7", "@tanstack/react-virtual": "^3.13.2", "react": "^19.0.0", + "react-intersection-observer": "^9.16.0", "zustand": "^4.3.2" }, "devDependencies": { @@ -2121,6 +2122,20 @@ "react": "^19.0.0" } }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", diff --git a/package.json b/package.json index 72851ae..24d10c2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@mui/material": "^6.4.7", "@tanstack/react-virtual": "^3.13.2", "react": "^19.0.0", + "react-intersection-observer": "^9.16.0", "zustand": "^4.3.2" }, "devDependencies": { diff --git a/src/common/VirtualizedList.tsx b/src/common/VirtualizedList.tsx index 1324683..c66d63b 100644 --- a/src/common/VirtualizedList.tsx +++ b/src/common/VirtualizedList.tsx @@ -1,92 +1,176 @@ -import React, { CSSProperties, useCallback, useRef } from 'react' +import React, { + CSSProperties, + ReactNode, + useCallback, + useEffect, + useRef, +} from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; +import { useInView } from "react-intersection-observer"; +import { QortalMetadata } from "../types/interfaces/resources"; interface PropsVirtualizedList { - list: any[] + list: any[]; children: (item: any, index: number) => React.ReactNode; + onSeenLastItem?: (item: QortalMetadata)=> void; } -export const VirtualizedList = ({list, children}: PropsVirtualizedList) => { - const parentRef = useRef(null); +export const VirtualizedList = ({ list, children, onSeenLastItem }: PropsVirtualizedList) => { + const parentRef = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: list.length, + getItemKey: useCallback( + (index: number) => + list[index]?.name && list[index]?.name + ? `${list[index].name}-${list[index].identifier}` + : list[index]?.id, + [list] + ), + getScrollElement: () => parentRef.current, + estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed + overscan: 10, // Number of items to render outside the visible area to improve smoothness + }); + + const onSeenLastItemFunc = useCallback((lastItem: QortalMetadata) => { + if(onSeenLastItem){ + onSeenLastItem(lastItem) + } - const rowVirtualizer = useVirtualizer({ - count: list.length, - getItemKey: useCallback((index: number) => (list[index]?.name && list[index]?.name) ?`${list[index].name}-${list[index].identifier}`: list[index]?.id, [list]), - getScrollElement: () => parentRef.current, - estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed - overscan: 10, // Number of items to render outside the visible area to improve smoothness - }); + }, []); return ( -
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const index = virtualRow.index; - const item = list[index]; - return ( -
- {typeof children === "function" ? children(item, index) : null} - -
- ); - })} +
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const index = virtualRow.index; + const item = list[index]; + return ( +
+ onSeenLastItemFunc(item)} + > + {typeof children === "function" + ? children(item, index) + : null} + +
+ ); + })} +
-
- ) + ); +}; + +interface MessageWrapperProps { + onSeen: () => void; + isLast: boolean; + children: ReactNode; } + +export const MessageWrapper: React.FC = ({ + onSeen, + isLast, + children, +}) => { + if (isLast) { + return ( + + {children} + + ); + } + return <>{children}; +}; + +interface WatchComponentProps { + onSeen: () => void; + isLast: boolean; + children: ReactNode; +} + +const WatchComponent: React.FC = ({ + onSeen, + isLast, + children, +}) => { + const { ref, inView } = useInView({ + threshold: 0.7, + triggerOnce: true, // Ensure it only triggers once per mount + }); + + const hasBeenTriggered = useRef(false); // Prevent multiple triggers + + useEffect(() => { + if (inView && isLast && onSeen && !hasBeenTriggered.current) { + onSeen(); + hasBeenTriggered.current = true; // Mark as triggered + } + }, [inView, isLast, onSeen]); + + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/ResourceList/ResourceListDisplay.tsx b/src/components/ResourceList/ResourceListDisplay.tsx index 9e5a752..9dc9bcd 100644 --- a/src/components/ResourceList/ResourceListDisplay.tsx +++ b/src/components/ResourceList/ResourceListDisplay.tsx @@ -37,6 +37,7 @@ interface PropsResourceListDisplay { defaultLoaderParams?: DefaultLoaderParams; loaderList?: (status: "LOADING" | "NO_RESULTS") => React.ReactNode; // Function type disableVirtualization?: boolean; + onSeenLastItem?: (listItem: QortalMetadata)=> void; } export const ResourceListDisplay = ({ @@ -49,6 +50,7 @@ export const ResourceListDisplay = ({ loaderItem, loaderList, disableVirtualization, + onSeenLastItem }: PropsResourceListDisplay) => { const [list, setList] = useState([]); const { fetchResources } = useResources(); @@ -94,7 +96,7 @@ export const ResourceListDisplay = ({ >
{!disableVirtualization && ( - + {(item: QortalMetadata, index: number) => ( <> {styles?.gap && }