about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/List.tsx64
-rw-r--r--src/view/com/util/MainScrollProvider.tsx97
-rw-r--r--src/view/com/util/ViewSelector.tsx11
-rw-r--r--src/view/com/util/Views.d.ts2
-rw-r--r--src/view/com/util/Views.jsx2
-rw-r--r--src/view/com/util/Views.web.tsx2
6 files changed, 170 insertions, 8 deletions
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
new file mode 100644
index 000000000..5947fe87a
--- /dev/null
+++ b/src/view/com/util/List.tsx
@@ -0,0 +1,64 @@
+import React, {memo, startTransition} from 'react'
+import {FlatListProps} from 'react-native'
+import {FlatList_INTERNAL} from './Views'
+import {useScrollHandlers} from '#/lib/ScrollContext'
+import {runOnJS, useSharedValue} from 'react-native-reanimated'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
+
+export type ListMethods = FlatList_INTERNAL
+export type ListProps<ItemT> = Omit<
+  FlatListProps<ItemT>,
+  'onScroll' // Use ScrollContext instead.
+> & {
+  onScrolledDownChange?: (isScrolledDown: boolean) => void
+}
+export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
+
+const SCROLLED_DOWN_LIMIT = 200
+
+function ListImpl<ItemT>(
+  {onScrolledDownChange, ...props}: ListProps<ItemT>,
+  ref: React.Ref<ListMethods>,
+) {
+  const isScrolledDown = useSharedValue(false)
+  const contextScrollHandlers = useScrollHandlers()
+
+  function handleScrolledDownChange(didScrollDown: boolean) {
+    startTransition(() => {
+      onScrolledDownChange?.(didScrollDown)
+    })
+  }
+
+  const scrollHandler = useAnimatedScrollHandler({
+    onBeginDrag(e, ctx) {
+      contextScrollHandlers.onBeginDrag?.(e, ctx)
+    },
+    onEndDrag(e, ctx) {
+      contextScrollHandlers.onEndDrag?.(e, ctx)
+    },
+    onScroll(e, ctx) {
+      contextScrollHandlers.onScroll?.(e, ctx)
+
+      const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
+      if (isScrolledDown.value !== didScrollDown) {
+        isScrolledDown.value = didScrollDown
+        if (onScrolledDownChange != null) {
+          runOnJS(handleScrolledDownChange)(didScrollDown)
+        }
+      }
+    },
+  })
+
+  return (
+    <FlatList_INTERNAL
+      {...props}
+      onScroll={scrollHandler}
+      scrollEventThrottle={1}
+      ref={ref}
+    />
+  )
+}
+
+export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
+  props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
+) => React.ReactElement
diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx
new file mode 100644
index 000000000..31a4ef0c8
--- /dev/null
+++ b/src/view/com/util/MainScrollProvider.tsx
@@ -0,0 +1,97 @@
+import React, {useCallback} from 'react'
+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 {useSharedValue, interpolate} from 'react-native-reanimated'
+
+function clamp(num: number, min: number, max: number) {
+  'worklet'
+  return Math.min(Math.max(num, min), max)
+}
+
+export function MainScrollProvider({children}: {children: React.ReactNode}) {
+  const {headerHeight} = useShellLayout()
+  const mode = useMinimalShellMode()
+  const setMode = useSetMinimalShellMode()
+  const startDragOffset = useSharedValue<number | null>(null)
+  const startMode = useSharedValue<number | null>(null)
+
+  const onBeginDrag = useCallback(
+    (e: NativeScrollEvent) => {
+      'worklet'
+      startDragOffset.value = e.contentOffset.y
+      startMode.value = mode.value
+    },
+    [mode, startDragOffset, startMode],
+  )
+
+  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)
+      }
+    },
+    [startDragOffset, startMode, setMode, mode, headerHeight],
+  )
+
+  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)
+          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.
+        startDragOffset.value = e.contentOffset.y
+        startMode.value = mode.value
+      }
+    },
+    [headerHeight, mode, setMode, startDragOffset, startMode],
+  )
+
+  return (
+    <ScrollProvider
+      onBeginDrag={onBeginDrag}
+      onEndDrag={onEndDrag}
+      onScroll={onScroll}>
+      {children}
+    </ScrollProvider>
+  )
+}
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index 935d93033..ee993c564 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -1,13 +1,14 @@
 import React, {useEffect, useState} from 'react'
 import {
+  NativeSyntheticEvent,
+  NativeScrollEvent,
   Pressable,
   RefreshControl,
   StyleSheet,
   View,
   ScrollView,
 } from 'react-native'
-import {FlatList} from './Views'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {FlatList_INTERNAL} from './Views'
 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
 import {Text} from './text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -38,7 +39,7 @@ export const ViewSelector = React.forwardRef<
       | null
       | undefined
     onSelectView?: (viewIndex: number) => void
-    onScroll?: OnScrollCb
+    onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
     onRefresh?: () => void
     onEndReached?: (info: {distanceFromEnd: number}) => void
   }
@@ -59,7 +60,7 @@ export const ViewSelector = React.forwardRef<
 ) {
   const pal = usePalette('default')
   const [selectedIndex, setSelectedIndex] = useState<number>(0)
-  const flatListRef = React.useRef<FlatList>(null)
+  const flatListRef = React.useRef<FlatList_INTERNAL>(null)
 
   // events
   // =
@@ -110,7 +111,7 @@ export const ViewSelector = React.forwardRef<
     [items],
   )
   return (
-    <FlatList
+    <FlatList_INTERNAL
       ref={flatListRef}
       data={data}
       keyExtractor={keyExtractor}
diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts
index 91df1d6bc..6a90cc229 100644
--- a/src/view/com/util/Views.d.ts
+++ b/src/view/com/util/Views.d.ts
@@ -1,6 +1,6 @@
 import React from 'react'
 import {ViewProps} from 'react-native'
-export {FlatList, ScrollView} from 'react-native'
+export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native'
 export function CenteredView({
   style,
   sideBorders,
diff --git a/src/view/com/util/Views.jsx b/src/view/com/util/Views.jsx
index 8a93ce511..7d6120583 100644
--- a/src/view/com/util/Views.jsx
+++ b/src/view/com/util/Views.jsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {View} from 'react-native'
 import Animated from 'react-native-reanimated'
 
-export const FlatList = Animated.FlatList
+export const FlatList_INTERNAL = Animated.FlatList
 export const ScrollView = Animated.ScrollView
 export function CenteredView(props) {
   return <View {...props} />
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index 5a4f266fd..db3b9de0d 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -49,7 +49,7 @@ export function CenteredView({
   return <View style={style} {...props} />
 }
 
-export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
+export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   {
     contentContainerStyle,
     style,