From 87a4f891a0127520665e13d51b909d004e076cd2 Mon Sep 17 00:00:00 2001
From: PhilReact <philliplangmartinez@gmail.com>
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 (
-    <div
-    style={{
-      display: "flex",
-      width: "100%",
-      height: "100%",
-    }}
-  >
     <div
       style={{
-        height: "100%",
-        position: "relative",
         display: "flex",
-        flexDirection: "column",
         width: "100%",
+        height: "100%",
       }}
     >
       <div
-        ref={parentRef}
-        className="List"
         style={{
-          flexGrow: 1,
-          overflow: "auto",
+          height: "100%",
           position: "relative",
           display: "flex",
-          height: "0px",
+          flexDirection: "column",
+          width: "100%",
         }}
       >
         <div
+          ref={parentRef}
+          className="List"
           style={{
-            height: rowVirtualizer.getTotalSize(),
-            width: "100%",
+            flexGrow: 1,
+            overflow: "auto",
+            position: "relative",
+            display: "flex",
+            height: "0px",
           }}
         >
           <div
             style={{
-              position: "absolute",
-              top: 0,
-              left: 0,
+              height: rowVirtualizer.getTotalSize(),
               width: "100%",
             }}
           >
-            {rowVirtualizer.getVirtualItems().map((virtualRow) => {
-              const index = virtualRow.index;
-              const item = list[index];
-              return (
-                <div
-                  data-index={virtualRow.index} //needed for dynamic row height measurement
-                  ref={rowVirtualizer.measureElement} //measure dynamic row height
-                  key={`${item.name}-${item.identifier}`}
-                  style={{
-                    position: "absolute",
-                    top: 0,
-                    left: "50%", // Move to the center horizontally
-                    transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
-                    width: "100%", // Control width (90% of the parent)
-                    display: "flex",
-                    alignItems: "center",
-                    overscrollBehavior: "none",
-                    flexDirection: "column"
-                  }}
-                >
-               {typeof children === "function" ? children(item, index) : null}
-
-                </div>
-              );
-            })}
+            <div
+              style={{
+                position: "absolute",
+                top: 0,
+                left: 0,
+                width: "100%",
+              }}
+            >
+              {rowVirtualizer.getVirtualItems().map((virtualRow) => {
+                const index = virtualRow.index;
+                const item = list[index];
+                return (
+                  <div
+                    data-index={virtualRow.index} //needed for dynamic row height measurement
+                    ref={rowVirtualizer.measureElement} //measure dynamic row height
+                    key={`${item.name}-${item.identifier}`}
+                    style={{
+                      position: "absolute",
+                      top: 0,
+                      left: "50%", // Move to the center horizontally
+                      transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
+                      width: "100%", // Control width (90% of the parent)
+                      display: "flex",
+                      alignItems: "center",
+                      overscrollBehavior: "none",
+                      flexDirection: "column",
+                    }}
+                  >
+                    <MessageWrapper
+                      isLast={index === list?.length - 1}
+                      onSeen={() => onSeenLastItemFunc(item)}
+                    >
+                      {typeof children === "function"
+                        ? children(item, index)
+                        : null}
+                    </MessageWrapper>
+                  </div>
+                );
+              })}
+            </div>
           </div>
         </div>
       </div>
     </div>
-  </div>
-  )
+  );
+};
+
+interface MessageWrapperProps {
+  onSeen: () => void;
+  isLast: boolean;
+  children: ReactNode;
 }
+
+export const MessageWrapper: React.FC<MessageWrapperProps> = ({
+  onSeen,
+  isLast,
+  children,
+}) => {
+  if (isLast) {
+    return (
+      <WatchComponent onSeen={onSeen} isLast={isLast}>
+        {children}
+      </WatchComponent>
+    );
+  }
+  return <>{children}</>;
+};
+
+interface WatchComponentProps {
+  onSeen: () => void;
+  isLast: boolean;
+  children: ReactNode;
+}
+
+const WatchComponent: React.FC<WatchComponentProps> = ({
+  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 (
+    <div
+      ref={ref}
+      style={{ width: "100%", display: "flex", justifyContent: "center" }}
+    >
+      {children}
+    </div>
+  );
+};
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<QortalMetadata[]>([]);
   const { fetchResources } = useResources();
@@ -94,7 +96,7 @@ export const ResourceListDisplay = ({
       >
         <div style={{ display: "flex", flexGrow: 1 }}>
           {!disableVirtualization && (
-            <VirtualizedList list={list}>
+            <VirtualizedList list={list} onSeenLastItem={onSeenLastItem}>
               {(item: QortalMetadata, index: number) => (
                 <>
                   {styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}