From e6d369f4312f0c8d175251e149c62d08608cb18c Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Mon, 4 Sep 2023 23:34:04 -0700
Subject: feat: :sparkles: autoscrolling

---
 extension/react-app/src/pages/gui.tsx | 93 +++++++++++++++++++++++++----------
 1 file changed, 66 insertions(+), 27 deletions(-)

(limited to 'extension/react-app/src/pages/gui.tsx')

diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index 60ad0c2c..3d98522a 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -99,24 +99,70 @@ function GUI(props: GUIProps) {
     );
   }, [bottomMessage, aboveComboBoxDivRef.current]);
 
-  const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
-    null
+  const [isScrolling, setIsScrolling] = useState(false);
+
+  const [scrollTimeoutId, setScrollTimeoutId] = useState<NodeJS.Timeout>();
+
+  const scrollToBottom = useCallback(
+    (force: boolean = false) => {
+      if (scrollTimeoutId) {
+        return;
+      }
+
+      // Scroll only if user is within 100 pixels of the bottom of the window.
+      const edgeOffset = 50;
+      const scrollPosition = window.scrollY || 0;
+      const scrollHeight = topGuiDivRef.current?.scrollHeight || 0;
+      const clientHeight = window.innerHeight || 0;
+
+      const atBottomEdge =
+        scrollPosition + clientHeight + edgeOffset >= scrollHeight;
+
+      if (isScrolling || (!atBottomEdge && !force)) {
+        return;
+      }
+
+      window.scrollTo({
+        top: scrollHeight,
+        behavior: "smooth",
+      });
+
+      setScrollTimeoutId(
+        setTimeout(() => {
+          setScrollTimeoutId(undefined);
+        }, 800)
+      );
+    },
+    [
+      window.scrollY,
+      topGuiDivRef.current?.scrollHeight,
+      topGuiDivRef.current?.clientHeight,
+      isScrolling,
+      scrollTimeoutId,
+    ]
   );
-  const scrollToBottom = useCallback(() => {
-    if (scrollTimeout) {
-      clearTimeout(scrollTimeout);
-    }
-    // Debounced smooth scroll to bottom of screen
-    if (topGuiDivRef.current) {
-      const timeout = setTimeout(() => {
-        window.scrollTo({
-          top: topGuiDivRef.current!.offsetHeight,
-          behavior: "smooth",
-        });
-      }, 200);
-      setScrollTimeout(timeout);
-    }
-  }, [topGuiDivRef.current, scrollTimeout]);
+
+  const [timerId, setTimerId] = useState<NodeJS.Timeout>();
+
+  useEffect(() => {
+    const handleScrollEnd = () => {
+      setIsScrolling(false);
+    };
+
+    const handleScroll = () => {
+      window.clearTimeout(timerId);
+
+      setIsScrolling(true);
+
+      setTimerId(setTimeout(() => handleScrollEnd(), 800));
+    };
+
+    window.addEventListener("scroll", handleScroll);
+
+    return () => {
+      window.removeEventListener("scroll", handleScroll);
+    };
+  }, []);
 
   useEffect(() => {
     // Cmd + Backspace to delete current step
@@ -141,11 +187,6 @@ function GUI(props: GUIProps) {
 
   useEffect(() => {
     client?.onStateUpdate((state: FullState) => {
-      // Scroll only if user is at very bottom of the window.
-      const shouldScrollToBottom =
-        topGuiDivRef.current &&
-        topGuiDivRef.current?.offsetHeight - window.scrollY < 100;
-
       const waitingForSteps =
         state.active &&
         state.history.current_index < state.history.timeline.length &&
@@ -177,14 +218,12 @@ function GUI(props: GUIProps) {
         return nextStepsOpen;
       });
 
-      if (shouldScrollToBottom) {
-        scrollToBottom();
-      }
+      scrollToBottom();
     });
   }, [client]);
 
   useEffect(() => {
-    scrollToBottom();
+    scrollToBottom(true);
   }, [waitingForSteps]);
 
   // #endregion
@@ -339,7 +378,7 @@ function GUI(props: GUIProps) {
   }, []);
   return (
     <div
-      className="overflow-hidden"
+      className="overflow-scroll"
       ref={topGuiDivRef}
       onKeyDown={(e) => {
         if (e.key === "Enter" && e.ctrlKey) {
-- 
cgit v1.2.3-70-g09d2

