about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/hooks/useAnimatedScrollHandler_FIXED.ts14
-rw-r--r--src/lib/hooks/useOnMainScroll.ts44
-rw-r--r--src/view/com/lists/ListItems.tsx8
-rw-r--r--src/view/com/notifications/Feed.tsx8
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx29
-rw-r--r--src/view/com/posts/Feed.tsx8
-rw-r--r--src/view/screens/ProfileFeed.tsx10
-rw-r--r--src/view/screens/ProfileList.tsx6
-rw-r--r--src/view/screens/SearchMobile.tsx6
9 files changed, 95 insertions, 38 deletions
diff --git a/src/lib/hooks/useAnimatedScrollHandler_FIXED.ts b/src/lib/hooks/useAnimatedScrollHandler_FIXED.ts
index eccfabbb0..56a1e8b11 100644
--- a/src/lib/hooks/useAnimatedScrollHandler_FIXED.ts
+++ b/src/lib/hooks/useAnimatedScrollHandler_FIXED.ts
@@ -1 +1,15 @@
+// Be warned. This Hook is very buggy unless used in a very constrained way.
+// To use it safely:
+//
+// - DO NOT pass its return value as a prop to any user-defined component.
+// - DO NOT pass its return value to more than a single component.
+//
+// In other words, the only safe way to use it is next to the leaf Reanimated View.
+//
+// Relevant bug reports:
+// - https://github.com/software-mansion/react-native-reanimated/issues/5345
+// - https://github.com/software-mansion/react-native-reanimated/issues/5360
+// - https://github.com/software-mansion/react-native-reanimated/issues/5364
+//
+// It's great when it works though.
 export {useAnimatedScrollHandler} from 'react-native-reanimated'
diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts
index 4cad34f40..2e7a79913 100644
--- a/src/lib/hooks/useOnMainScroll.ts
+++ b/src/lib/hooks/useOnMainScroll.ts
@@ -1,11 +1,15 @@
-import {useState, useCallback} from 'react'
+import {useState, useCallback, useMemo} from 'react'
 import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
 import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {s} from 'lib/styles'
 import {isWeb} from 'platform/detection'
-import {useSharedValue, interpolate, runOnJS} from 'react-native-reanimated'
-import {useAnimatedScrollHandler} from './useAnimatedScrollHandler_FIXED'
+import {
+  useSharedValue,
+  interpolate,
+  runOnJS,
+  ScrollHandlers,
+} from 'react-native-reanimated'
 
 function clamp(num: number, min: number, max: number) {
   'worklet'
@@ -15,9 +19,10 @@ function clamp(num: number, min: number, max: number) {
 export type OnScrollCb = (
   event: NativeSyntheticEvent<NativeScrollEvent>,
 ) => void
+export type OnScrollHandler = ScrollHandlers<any>
 export type ResetCb = () => void
 
-export function useOnMainScroll(): [OnScrollCb, boolean, ResetCb] {
+export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] {
   const {headerHeight} = useShellLayout()
   const [isScrolledDown, setIsScrolledDown] = useState(false)
   const mode = useMinimalShellMode()
@@ -25,12 +30,18 @@ export function useOnMainScroll(): [OnScrollCb, boolean, ResetCb] {
   const startDragOffset = useSharedValue<number | null>(null)
   const startMode = useSharedValue<number | null>(null)
 
-  const scrollHandler = useAnimatedScrollHandler({
-    onBeginDrag(e) {
+  const onBeginDrag = useCallback(
+    (e: NativeScrollEvent) => {
+      'worklet'
       startDragOffset.value = e.contentOffset.y
       startMode.value = mode.value
     },
-    onEndDrag(e) {
+    [mode, startDragOffset, startMode],
+  )
+
+  const onEndDrag = useCallback(
+    (e: NativeScrollEvent) => {
+      'worklet'
       startDragOffset.value = null
       startMode.value = null
       if (e.contentOffset.y < headerHeight.value / 2) {
@@ -41,7 +52,12 @@ export function useOnMainScroll(): [OnScrollCb, boolean, ResetCb] {
         setMode(Math.round(mode.value) === 1)
       }
     },
-    onScroll(e) {
+    [startDragOffset, startMode, setMode, mode, headerHeight],
+  )
+
+  const onScroll = useCallback(
+    (e: NativeScrollEvent) => {
+      'worklet'
       // Keep track of whether we want to show "scroll to top".
       if (!isScrolledDown && e.contentOffset.y > s.window.height) {
         runOnJS(setIsScrolledDown)(true)
@@ -86,7 +102,17 @@ export function useOnMainScroll(): [OnScrollCb, boolean, ResetCb] {
         startMode.value = mode.value
       }
     },
-  })
+    [headerHeight, mode, setMode, isScrolledDown, startDragOffset, startMode],
+  )
+
+  const scrollHandler: ScrollHandlers<any> = useMemo(
+    () => ({
+      onBeginDrag,
+      onEndDrag,
+      onScroll,
+    }),
+    [onBeginDrag, onEndDrag, onScroll],
+  )
 
   return [
     scrollHandler,
diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx
index 3658e5522..fe7b9b78a 100644
--- a/src/view/com/lists/ListItems.tsx
+++ b/src/view/com/lists/ListItems.tsx
@@ -19,9 +19,10 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s} from 'lib/styles'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {logger} from '#/logger'
 import {useModalControls} from '#/state/modals'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_ITEM = {_reactKey: '__empty__'}
@@ -44,7 +45,7 @@ export const ListItems = observer(function ListItemsImpl({
   list: ListModel
   style?: StyleProp<ViewStyle>
   scrollElRef?: MutableRefObject<FlatList<any> | null>
-  onScroll?: OnScrollCb
+  onScroll: OnScrollHandler
   onPressTryAgain?: () => void
   renderHeader: () => JSX.Element
   renderEmptyState: () => JSX.Element
@@ -205,6 +206,7 @@ export const ListItems = observer(function ListItemsImpl({
     [list.isLoading],
   )
 
+  const scrollHandler = useAnimatedScrollHandler(onScroll)
   return (
     <View testID={testID} style={style}>
       <FlatList
@@ -226,7 +228,7 @@ export const ListItems = observer(function ListItemsImpl({
         }
         contentContainerStyle={s.contentContainer}
         style={{paddingTop: headerOffset}}
-        onScroll={onScroll}
+        onScroll={scrollHandler}
         onEndReached={onEndReached}
         onEndReachedThreshold={0.6}
         scrollEventThrottle={scrollEventThrottle}
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index dff84ec77..4794a9867 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -8,7 +8,8 @@ import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
 import {EmptyState} from '../util/EmptyState'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {logger} from '#/logger'
@@ -27,7 +28,7 @@ export const Feed = observer(function Feed({
   view: NotificationsFeedModel
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressTryAgain?: () => void
-  onScroll?: OnScrollCb
+  onScroll?: OnScrollHandler
   ListHeaderComponent?: () => JSX.Element
 }) {
   const pal = usePalette('default')
@@ -129,6 +130,7 @@ export const Feed = observer(function Feed({
     [view],
   )
 
+  const scrollHandler = useAnimatedScrollHandler(onScroll || {})
   return (
     <View style={s.hContentRegion}>
       <CenteredView>
@@ -161,7 +163,7 @@ export const Feed = observer(function Feed({
           }
           onEndReached={onEndReached}
           onEndReachedThreshold={0.6}
-          onScroll={onScroll}
+          onScroll={scrollHandler}
           scrollEventThrottle={1}
           contentContainerStyle={s.contentContainer}
           // @ts-ignore our .web version only -prf
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index f3ea4a1d1..8b9e0c85a 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -1,5 +1,10 @@
 import * as React from 'react'
-import {LayoutChangeEvent, StyleSheet, View} from 'react-native'
+import {
+  LayoutChangeEvent,
+  NativeScrollEvent,
+  StyleSheet,
+  View,
+} from 'react-native'
 import Animated, {
   Easing,
   useAnimatedReaction,
@@ -11,14 +16,13 @@ import Animated, {
 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {TabBar} from './TabBar'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
-import {useAnimatedScrollHandler} from 'lib/hooks/useAnimatedScrollHandler_FIXED'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 
 const SCROLLED_DOWN_LIMIT = 200
 
 interface PagerWithHeaderChildParams {
   headerHeight: number
-  onScroll: OnScrollCb
+  onScroll: OnScrollHandler
   isScrolledDown: boolean
 }
 
@@ -141,11 +145,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
     )
 
     // props to pass into children render functions
-    const onScroll = useAnimatedScrollHandler({
-      onScroll(e) {
-        scrollY.value = e.contentOffset.y
-      },
-    })
+    function onScrollWorklet(e: NativeScrollEvent) {
+      'worklet'
+      scrollY.value = e.contentOffset.y
+    }
 
     const onPageSelectedInner = React.useCallback(
       (index: number) => {
@@ -192,7 +195,9 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
               output = child({
                 headerHeight,
                 isScrolledDown,
-                onScroll: i === currentPage ? onScroll : noop,
+                onScroll: {
+                  onScroll: i === currentPage ? onScrollWorklet : noop,
+                },
               })
             }
             // Pager children must be noncollapsible plain <View>s.
@@ -225,7 +230,9 @@ const styles = StyleSheet.create({
   },
 })
 
-function noop() {}
+function noop() {
+  'worklet'
+}
 
 function toArray<T>(v: T | T[]): T[] {
   if (Array.isArray(v)) {
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 1ecb14912..5b517f4c7 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -14,10 +14,11 @@ import {FeedErrorMessage} from './FeedErrorMessage'
 import {PostsFeedModel} from 'state/models/feeds/posts'
 import {FeedSlice} from './FeedSlice'
 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {s} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {useTheme} from 'lib/ThemeContext'
 import {logger} from '#/logger'
 
@@ -43,7 +44,7 @@ export const Feed = observer(function Feed({
   feed: PostsFeedModel
   style?: StyleProp<ViewStyle>
   scrollElRef?: MutableRefObject<FlatList<any> | null>
-  onScroll?: OnScrollCb
+  onScroll?: OnScrollHandler
   scrollEventThrottle?: number
   renderEmptyState: () => JSX.Element
   renderEndOfFeed?: () => JSX.Element
@@ -157,6 +158,7 @@ export const Feed = observer(function Feed({
     [feed.isLoadingMore, feed.hasMore, feed.isEmpty, renderEndOfFeed],
   )
 
+  const scrollHandler = useAnimatedScrollHandler(onScroll || {})
   return (
     <View testID={testID} style={style}>
       <FlatList
@@ -178,7 +180,7 @@ export const Feed = observer(function Feed({
         }
         contentContainerStyle={s.contentContainer}
         style={{paddingTop: headerOffset}}
-        onScroll={onScroll}
+        onScroll={onScroll != null ? scrollHandler : undefined}
         scrollEventThrottle={scrollEventThrottle}
         indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
         onEndReached={onEndReached}
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 9c3c6d7ae..c1496e4ad 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -26,7 +26,7 @@ import {EmptyState} from 'view/com/util/EmptyState'
 import * as Toast from 'view/com/util/Toast'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {useCustomFeed} from 'lib/hooks/useCustomFeed'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {shareUrl} from 'lib/sharing'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {Haptics} from 'lib/haptics'
@@ -44,6 +44,7 @@ import {logger} from '#/logger'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 
 const SECTION_TITLES = ['Posts', 'About']
 
@@ -383,7 +384,7 @@ export const ProfileFeedScreenInner = observer(
 
 interface FeedSectionProps {
   feed: PostsFeedModel
-  onScroll: OnScrollCb
+  onScroll: OnScrollHandler
   headerHeight: number
   isScrolledDown: boolean
 }
@@ -443,10 +444,11 @@ const AboutSection = observer(function AboutPageImpl({
   feedInfo: FeedSourceModel | undefined
   headerHeight: number
   onToggleLiked: () => void
-  onScroll: OnScrollCb
+  onScroll: OnScrollHandler
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
+  const scrollHandler = useAnimatedScrollHandler(onScroll)
 
   if (!feedInfo) {
     return <View />
@@ -456,7 +458,7 @@ const AboutSection = observer(function AboutPageImpl({
     <ScrollView
       scrollEventThrottle={1}
       contentContainerStyle={{paddingTop: headerHeight}}
-      onScroll={onScroll}>
+      onScroll={scrollHandler}>
       <View
         style={[
           {
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index e473d7338..497c1ae76 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -33,7 +33,7 @@ import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {NavigationProp} from 'lib/routes/types'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {shareUrl} from 'lib/sharing'
@@ -554,7 +554,7 @@ const Header = observer(function HeaderImpl({
 
 interface FeedSectionProps {
   feed: PostsFeedModel
-  onScroll: OnScrollCb
+  onScroll: OnScrollHandler
   headerHeight: number
   isScrolledDown: boolean
 }
@@ -608,7 +608,7 @@ interface AboutSectionProps {
   isCurateList: boolean | undefined
   isOwner: boolean | undefined
   onPressAddUser: () => void
-  onScroll: OnScrollCb
+  onScroll: OnScrollHandler
   headerHeight: number
   isScrolledDown: boolean
 }
diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx
index c1df58ffd..92c255d5b 100644
--- a/src/view/screens/SearchMobile.tsx
+++ b/src/view/screens/SearchMobile.tsx
@@ -14,6 +14,7 @@ import {
 } from 'lib/routes/types'
 import {observer} from 'mobx-react-lite'
 import {Text} from 'view/com/util/text/Text'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {useStores} from 'state/index'
 import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {SearchUIModel} from 'state/models/ui/search'
@@ -131,6 +132,7 @@ export const SearchScreen = withAuthRequired(
       }
     }, [])
 
+    const scrollHandler = useAnimatedScrollHandler(onMainScroll)
     return (
       <TouchableWithoutFeedback onPress={onPress} accessible={false}>
         <View style={[pal.view, styles.container]}>
@@ -156,8 +158,8 @@ export const SearchScreen = withAuthRequired(
               ref={scrollViewRef}
               testID="searchScrollView"
               style={pal.view}
-              onScroll={onMainScroll}
-              scrollEventThrottle={100}>
+              onScroll={scrollHandler}
+              scrollEventThrottle={1}>
               {query && autocompleteView.suggestions.length ? (
                 <>
                   {autocompleteView.suggestions.map((suggestion, index) => (