about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/lib/hooks/useMinimalShellTransform.ts45
-rw-r--r--src/state/shell/minimal-mode.tsx43
-rw-r--r--src/view/com/util/MainScrollProvider.tsx53
-rw-r--r--src/view/screens/Home.tsx6
4 files changed, 95 insertions, 52 deletions
diff --git a/src/lib/hooks/useMinimalShellTransform.ts b/src/lib/hooks/useMinimalShellTransform.ts
index 17fe058e9..678776755 100644
--- a/src/lib/hooks/useMinimalShellTransform.ts
+++ b/src/lib/hooks/useMinimalShellTransform.ts
@@ -2,21 +2,24 @@ import {interpolate, useAnimatedStyle} from 'react-native-reanimated'
 
 import {useMinimalShellMode} from '#/state/shell/minimal-mode'
 import {useShellLayout} from '#/state/shell/shell-layout'
-import {useGate} from '../statsig/statsig'
 
 // Keep these separated so that we only pay for useAnimatedStyle that gets used.
 
 export function useMinimalShellHeaderTransform() {
-  const mode = useMinimalShellMode()
+  const {headerMode} = useMinimalShellMode()
   const {headerHeight} = useShellLayout()
 
   const headerTransform = useAnimatedStyle(() => {
     return {
-      pointerEvents: mode.value === 0 ? 'auto' : 'none',
-      opacity: Math.pow(1 - mode.value, 2),
+      pointerEvents: headerMode.value === 0 ? 'auto' : 'none',
+      opacity: Math.pow(1 - headerMode.value, 2),
       transform: [
         {
-          translateY: interpolate(mode.value, [0, 1], [0, -headerHeight.value]),
+          translateY: interpolate(
+            headerMode.value,
+            [0, 1],
+            [0, -headerHeight.value],
+          ),
         },
       ],
     }
@@ -26,21 +29,20 @@ export function useMinimalShellHeaderTransform() {
 }
 
 export function useMinimalShellFooterTransform() {
-  const mode = useMinimalShellMode()
+  const {footerMode} = useMinimalShellMode()
   const {footerHeight} = useShellLayout()
-  const gate = useGate()
-  const isFixedBottomBar = gate('fixed_bottom_bar')
 
   const footerTransform = useAnimatedStyle(() => {
-    if (isFixedBottomBar) {
-      return {}
-    }
     return {
-      pointerEvents: mode.value === 0 ? 'auto' : 'none',
-      opacity: Math.pow(1 - mode.value, 2),
+      pointerEvents: footerMode.value === 0 ? 'auto' : 'none',
+      opacity: Math.pow(1 - footerMode.value, 2),
       transform: [
         {
-          translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]),
+          translateY: interpolate(
+            footerMode.value,
+            [0, 1],
+            [0, footerHeight.value],
+          ),
         },
       ],
     }
@@ -50,24 +52,13 @@ export function useMinimalShellFooterTransform() {
 }
 
 export function useMinimalShellFabTransform() {
-  const mode = useMinimalShellMode()
-  const gate = useGate()
-  const isFixedBottomBar = gate('fixed_bottom_bar')
+  const {footerMode} = useMinimalShellMode()
 
   const fabTransform = useAnimatedStyle(() => {
-    if (isFixedBottomBar) {
-      return {
-        transform: [
-          {
-            translateY: -44,
-          },
-        ],
-      }
-    }
     return {
       transform: [
         {
-          translateY: interpolate(mode.value, [0, 1], [-44, 0]),
+          translateY: interpolate(footerMode.value, [0, 1], [-44, 0]),
         },
       ],
     }
diff --git a/src/state/shell/minimal-mode.tsx b/src/state/shell/minimal-mode.tsx
index 69ce13062..9230339dd 100644
--- a/src/state/shell/minimal-mode.tsx
+++ b/src/state/shell/minimal-mode.tsx
@@ -6,32 +6,55 @@ import {
   withSpring,
 } from 'react-native-reanimated'
 
-type StateContext = SharedValue<number>
+type StateContext = {
+  headerMode: SharedValue<number>
+  footerMode: SharedValue<number>
+}
 type SetContext = (v: boolean) => void
 
 const stateContext = React.createContext<StateContext>({
-  value: 0,
-  addListener() {},
-  removeListener() {},
-  modify() {},
+  headerMode: {
+    value: 0,
+    addListener() {},
+    removeListener() {},
+    modify() {},
+  },
+  footerMode: {
+    value: 0,
+    addListener() {},
+    removeListener() {},
+    modify() {},
+  },
 })
 const setContext = React.createContext<SetContext>((_: boolean) => {})
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
-  const mode = useSharedValue(0)
+  const headerMode = useSharedValue(0)
+  const footerMode = useSharedValue(0)
   const setMode = React.useCallback(
     (v: boolean) => {
       'worklet'
       // Cancel any existing animation
-      cancelAnimation(mode)
-      mode.value = withSpring(v ? 1 : 0, {
+      cancelAnimation(headerMode)
+      headerMode.value = withSpring(v ? 1 : 0, {
+        overshootClamping: true,
+      })
+      cancelAnimation(footerMode)
+      footerMode.value = withSpring(v ? 1 : 0, {
         overshootClamping: true,
       })
     },
-    [mode],
+    [headerMode, footerMode],
+  )
+  const value = React.useMemo(
+    () => ({
+      headerMode,
+      footerMode,
+    }),
+    [headerMode, footerMode],
   )
   return (
-    <stateContext.Provider value={mode}>
+    <stateContext.Provider value={value}>
       <setContext.Provider value={setMode}>{children}</setContext.Provider>
     </stateContext.Provider>
   )
diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx
index b602da432..3163d8544 100644
--- a/src/view/com/util/MainScrollProvider.tsx
+++ b/src/view/com/util/MainScrollProvider.tsx
@@ -4,11 +4,13 @@ import {
   cancelAnimation,
   interpolate,
   useSharedValue,
+  withSpring,
 } from 'react-native-reanimated'
 import EventEmitter from 'eventemitter3'
 
 import {ScrollProvider} from '#/lib/ScrollContext'
-import {useMinimalShellMode, useSetMinimalShellMode} from '#/state/shell'
+import {useGate} from '#/lib/statsig/statsig'
+import {useMinimalShellMode} from '#/state/shell'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {isNative, isWeb} from 'platform/detection'
 
@@ -21,11 +23,29 @@ function clamp(num: number, min: number, max: number) {
 
 export function MainScrollProvider({children}: {children: React.ReactNode}) {
   const {headerHeight} = useShellLayout()
-  const mode = useMinimalShellMode()
-  const setMode = useSetMinimalShellMode()
+  const {headerMode, footerMode} = useMinimalShellMode()
   const startDragOffset = useSharedValue<number | null>(null)
   const startMode = useSharedValue<number | null>(null)
   const didJustRestoreScroll = useSharedValue<boolean>(false)
+  const gate = useGate()
+  const isFixedBottomBar = gate('fixed_bottom_bar')
+
+  const setMode = React.useCallback(
+    (v: boolean) => {
+      'worklet'
+      cancelAnimation(headerMode)
+      headerMode.value = withSpring(v ? 1 : 0, {
+        overshootClamping: true,
+      })
+      if (!isFixedBottomBar) {
+        cancelAnimation(footerMode)
+        footerMode.value = withSpring(v ? 1 : 0, {
+          overshootClamping: true,
+        })
+      }
+    },
+    [headerMode, footerMode, isFixedBottomBar],
+  )
 
   useEffect(() => {
     if (isWeb) {
@@ -55,11 +75,11 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
           setMode(true)
         } else {
           // Snap to whichever state is the closest.
-          setMode(Math.round(mode.value) === 1)
+          setMode(Math.round(headerMode.value) === 1)
         }
       }
     },
-    [startDragOffset, startMode, setMode, mode, headerHeight],
+    [startDragOffset, startMode, setMode, headerMode, headerHeight],
   )
 
   const onBeginDrag = useCallback(
@@ -67,10 +87,10 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
       'worklet'
       if (isNative) {
         startDragOffset.value = e.contentOffset.y
-        startMode.value = mode.value
+        startMode.value = headerMode.value
       }
     },
-    [mode, startDragOffset, startMode],
+    [headerMode, startDragOffset, startMode],
   )
 
   const onEndDrag = useCallback(
@@ -102,7 +122,10 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
       'worklet'
       if (isNative) {
         if (startDragOffset.value === null || startMode.value === null) {
-          if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
+          if (
+            headerMode.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)
@@ -119,11 +142,15 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
           [-1, 1],
         )
         const newValue = clamp(startMode.value + dProgress, 0, 1)
-        if (newValue !== mode.value) {
+        if (newValue !== headerMode.value) {
           // Manually adjust the value. This won't be (and shouldn't be) animated.
           // Cancel any any existing animation
-          cancelAnimation(mode)
-          mode.value = newValue
+          cancelAnimation(headerMode)
+          headerMode.value = newValue
+          if (!isFixedBottomBar) {
+            cancelAnimation(footerMode)
+            footerMode.value = newValue
+          }
         }
       } else {
         if (didJustRestoreScroll.value) {
@@ -145,11 +172,13 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
     },
     [
       headerHeight,
-      mode,
+      headerMode,
+      footerMode,
       setMode,
       startDragOffset,
       startMode,
       didJustRestoreScroll,
+      isFixedBottomBar,
     ],
   )
 
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 9a47007c4..af424428d 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -167,7 +167,7 @@ function HomeScreenReady({
     }),
   )
 
-  const mode = useMinimalShellMode()
+  const {footerMode} = useMinimalShellMode()
   const {isMobile} = useWebMediaQueries()
   useFocusEffect(
     React.useCallback(() => {
@@ -177,7 +177,7 @@ function HomeScreenReady({
       }
       const listener = AppState.addEventListener('change', nextAppState => {
         if (nextAppState === 'active') {
-          if (isMobile && mode.value === 1) {
+          if (isMobile && footerMode.value === 1) {
             // Reveal the bottom bar so you don't miss notifications or messages.
             // TODO: Experiment with only doing it when unread > 0.
             setMinimalShellMode(false)
@@ -187,7 +187,7 @@ function HomeScreenReady({
       return () => {
         listener.remove()
       }
-    }, [setMinimalShellMode, mode, isMobile, gate]),
+    }, [setMinimalShellMode, footerMode, isMobile, gate]),
   )
 
   const onPageSelected = React.useCallback(