From b147f7ae8a5c8c3a1ec9981132b4c6447034963f Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Thu, 11 Jan 2024 18:44:56 +0000
Subject: [PATCH] Toggle minimal shell on any scroll for web (#2499)

---
 src/view/com/util/MainScrollProvider.tsx | 89 +++++++++++++-----------
 1 file changed, 47 insertions(+), 42 deletions(-)

diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx
index 31a4ef0c..3ac28d31 100644
--- a/src/view/com/util/MainScrollProvider.tsx
+++ b/src/view/com/util/MainScrollProvider.tsx
@@ -3,9 +3,11 @@ import {ScrollProvider} from '#/lib/ScrollContext'
 import {NativeScrollEvent} from 'react-native'
 import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
 import {useShellLayout} from '#/state/shell/shell-layout'
-import {isWeb} from 'platform/detection'
+import {isNative} from 'platform/detection'
 import {useSharedValue, interpolate} from 'react-native-reanimated'
 
+const WEB_HIDE_SHELL_THRESHOLD = 200
+
 function clamp(num: number, min: number, max: number) {
   'worklet'
   return Math.min(Math.max(num, min), max)
@@ -21,8 +23,10 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
   const onBeginDrag = useCallback(
     (e: NativeScrollEvent) => {
       'worklet'
-      startDragOffset.value = e.contentOffset.y
-      startMode.value = mode.value
+      if (isNative) {
+        startDragOffset.value = e.contentOffset.y
+        startMode.value = mode.value
+      }
     },
     [mode, startDragOffset, startMode],
   )
@@ -30,14 +34,16 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
   const onEndDrag = useCallback(
     (e: NativeScrollEvent) => {
       'worklet'
-      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)
+      if (isNative) {
+        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)
+        }
       }
     },
     [startDragOffset, startMode, setMode, mode, headerHeight],
@@ -46,41 +52,40 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
   const onScroll = useCallback(
     (e: NativeScrollEvent) => {
       'worklet'
-      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)
+      if (isNative) {
+        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 (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
-        }
-        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.
+        // 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
+        }
+      } else {
+        // On the web, we don't try to follow the drag because we don't know when it ends.
+        // Instead, show/hide immediately based on whether we're scrolling up or down.
+        const dy = e.contentOffset.y - (startDragOffset.value ?? 0)
         startDragOffset.value = e.contentOffset.y
-        startMode.value = mode.value
+
+        if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) {
+          setMode(false)
+        } else if (dy > 0) {
+          setMode(true)
+        }
       }
     },
     [headerHeight, mode, setMode, startDragOffset, startMode],