about summary refs log tree commit diff
path: root/src/lib/hooks/useOnMainScroll.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/hooks/useOnMainScroll.ts')
-rw-r--r--src/lib/hooks/useOnMainScroll.ts135
1 files changed, 83 insertions, 52 deletions
diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts
index a213d5317..cc07329ea 100644
--- a/src/lib/hooks/useOnMainScroll.ts
+++ b/src/lib/hooks/useOnMainScroll.ts
@@ -1,17 +1,19 @@
-import {useState, useCallback, useRef} from 'react'
+import {useState, useCallback} from 'react'
 import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
-import {s} from 'lib/styles'
-import {useWebMediaQueries} from './useWebMediaQueries'
 import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
+import {useShellLayout} from '#/state/shell/shell-layout'
+import {s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
+import {
+  useAnimatedScrollHandler,
+  useSharedValue,
+  interpolate,
+  runOnJS,
+} from 'react-native-reanimated'
 
-const Y_LIMIT = 10
-
-const useDeviceLimits = () => {
-  const {isDesktop} = useWebMediaQueries()
-  return {
-    dyLimitUp: isDesktop ? 30 : 10,
-    dyLimitDown: isDesktop ? 150 : 10,
-  }
+function clamp(num: number, min: number, max: number) {
+  'worklet'
+  return Math.min(Math.max(num, min), max)
 }
 
 export type OnScrollCb = (
@@ -20,53 +22,82 @@ export type OnScrollCb = (
 export type ResetCb = () => void
 
 export function useOnMainScroll(): [OnScrollCb, boolean, ResetCb] {
-  let lastY = useRef(0)
-  let [isScrolledDown, setIsScrolledDown] = useState(false)
-  const {dyLimitUp, dyLimitDown} = useDeviceLimits()
-  const minimalShellMode = useMinimalShellMode()
-  const setMinimalShellMode = useSetMinimalShellMode()
+  const {headerHeight} = useShellLayout()
+  const [isScrolledDown, setIsScrolledDown] = useState(false)
+  const mode = useMinimalShellMode()
+  const setMode = useSetMinimalShellMode()
+  const startDragOffset = useSharedValue<number | null>(null)
+  const startMode = useSharedValue<number | null>(null)
 
-  return [
-    useCallback(
-      (event: NativeSyntheticEvent<NativeScrollEvent>) => {
-        const y = event.nativeEvent.contentOffset.y
-        const dy = y - (lastY.current || 0)
-        lastY.current = y
+  const scrollHandler = useAnimatedScrollHandler({
+    onBeginDrag(e) {
+      startDragOffset.value = e.contentOffset.y
+      startMode.value = mode.value
+    },
+    onEndDrag(e) {
+      startDragOffset.value = null
+      startMode.value = null
+      if (e.contentOffset.y < headerHeight.value / 2) {
+        // If we're close to the top, show the shell.
+        setMode(false)
+      } else {
+        // Snap to whichever state is the closest.
+        setMode(Math.round(mode.value) === 1)
+      }
+    },
+    onScroll(e) {
+      // Keep track of whether we want to show "scroll to top".
+      if (!isScrolledDown && e.contentOffset.y > s.window.height) {
+        runOnJS(setIsScrolledDown)(true)
+      } else if (isScrolledDown && e.contentOffset.y < s.window.height) {
+        runOnJS(setIsScrolledDown)(false)
+      }
 
-        if (!minimalShellMode.value && dy > dyLimitDown && y > Y_LIMIT) {
-          setMinimalShellMode(true)
-        } else if (
-          minimalShellMode.value &&
-          (dy < dyLimitUp * -1 || y <= Y_LIMIT)
-        ) {
-          setMinimalShellMode(false)
+      if (startDragOffset.value === null || startMode.value === null) {
+        if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
+          // If we're close enough to the top, always show the shell.
+          // Even if we're not dragging.
+          setMode(false)
+          return
         }
-
-        if (
-          !isScrolledDown &&
-          event.nativeEvent.contentOffset.y > s.window.height
-        ) {
-          setIsScrolledDown(true)
-        } else if (
-          isScrolledDown &&
-          event.nativeEvent.contentOffset.y < s.window.height
-        ) {
-          setIsScrolledDown(false)
+        if (isWeb) {
+          // On the web, there is no concept of "starting" the drag.
+          // When we get the first scroll event, we consider that the start.
+          startDragOffset.value = e.contentOffset.y
+          startMode.value = mode.value
         }
-      },
-      [
-        dyLimitDown,
-        dyLimitUp,
-        isScrolledDown,
-        minimalShellMode,
-        setMinimalShellMode,
-      ],
-    ),
+        return
+      }
+
+      // The "mode" value is always between 0 and 1.
+      // Figure out how much to move it based on the current dragged distance.
+      const dy = e.contentOffset.y - startDragOffset.value
+      const dProgress = interpolate(
+        dy,
+        [-headerHeight.value, headerHeight.value],
+        [-1, 1],
+      )
+      const newValue = clamp(startMode.value + dProgress, 0, 1)
+      if (newValue !== mode.value) {
+        // Manually adjust the value. This won't be (and shouldn't be) animated.
+        mode.value = newValue
+      }
+      if (isWeb) {
+        // On the web, there is no concept of "starting" the drag,
+        // so we don't have any specific anchor point to calculate the distance.
+        // Instead, update it continuosly along the way and diff with the last event.
+        startDragOffset.value = e.contentOffset.y
+        startMode.value = mode.value
+      }
+    },
+  })
+
+  return [
+    scrollHandler,
     isScrolledDown,
     useCallback(() => {
       setIsScrolledDown(false)
-      setMinimalShellMode(false)
-      lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf
-    }, [setIsScrolledDown, setMinimalShellMode]),
+      setMode(false)
+    }, [setMode]),
   ]
 }