about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/screens/Messages/components/MessagesList.tsx6
-rw-r--r--src/screens/Onboarding/Layout.tsx3
-rw-r--r--src/view/com/pager/PagerWithHeader.web.tsx4
-rw-r--r--src/view/com/util/List.tsx240
-rw-r--r--src/view/com/util/ViewSelector.tsx1
-rw-r--r--src/view/com/util/Views.d.ts19
-rw-r--r--src/view/com/util/Views.jsx7
-rw-r--r--src/view/com/util/Views.tsx28
-rw-r--r--src/view/screens/Feeds.tsx6
-rw-r--r--src/view/screens/Storybook/ListContained.tsx6
10 files changed, 168 insertions, 152 deletions
diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx
index 067abb27e..5edea2411 100644
--- a/src/screens/Messages/components/MessagesList.tsx
+++ b/src/screens/Messages/components/MessagesList.tsx
@@ -1,5 +1,5 @@
 import React, {useCallback, useRef} from 'react'
-import {FlatList, LayoutChangeEvent, View} from 'react-native'
+import {LayoutChangeEvent, View} from 'react-native'
 import {
   KeyboardStickyView,
   useKeyboardHandler,
@@ -33,7 +33,7 @@ import {
   EmojiPicker,
   EmojiPickerState,
 } from '#/view/com/composer/text-input/web/EmojiPicker.web'
-import {List} from '#/view/com/util/List'
+import {List, ListMethods} from '#/view/com/util/List'
 import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
 import {MessageInput} from '#/screens/Messages/components/MessageInput'
 import {MessageListError} from '#/screens/Messages/components/MessageListError'
@@ -94,7 +94,7 @@ export function MessagesList({
   const getPost = useGetPost()
   const {embedUri, setEmbed} = useMessageEmbed()
 
-  const flatListRef = useAnimatedRef<FlatList>()
+  const flatListRef = useAnimatedRef<ListMethods>()
 
   const [newMessagesPill, setNewMessagesPill] = React.useState({
     show: false,
diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx
index 4a07ebd83..54821532c 100644
--- a/src/screens/Onboarding/Layout.tsx
+++ b/src/screens/Onboarding/Layout.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
 import {View} from 'react-native'
+import Animated from 'react-native-reanimated'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -35,7 +36,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
   const {gtMobile} = useBreakpoints()
   const onboardDispatch = useOnboardingDispatch()
   const {state, dispatch} = React.useContext(Context)
-  const scrollview = React.useRef<ScrollView>(null)
+  const scrollview = React.useRef<Animated.ScrollView>(null)
   const prevActiveStep = React.useRef<string>(state.activeStep)
 
   React.useEffect(() => {
diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx
index acf4f1784..e72c1f3cc 100644
--- a/src/view/com/pager/PagerWithHeader.web.tsx
+++ b/src/view/com/pager/PagerWithHeader.web.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import {FlatList, ScrollView, StyleSheet, View} from 'react-native'
+import {ScrollView, StyleSheet, View} from 'react-native'
 import {useAnimatedRef} from 'react-native-reanimated'
 
 import {usePalette} from '#/lib/hooks/usePalette'
@@ -11,7 +11,7 @@ import {TabBar} from './TabBar'
 export interface PagerWithHeaderChildParams {
   headerHeight: number
   isFocused: boolean
-  scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
+  scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null>
 }
 
 export interface PagerWithHeaderProps {
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index fa93ec5e6..8176d0b43 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -1,6 +1,10 @@
 import React, {memo} from 'react'
-import {FlatListProps, RefreshControl, ViewToken} from 'react-native'
-import {runOnJS, useSharedValue} from 'react-native-reanimated'
+import {RefreshControl, ViewToken} from 'react-native'
+import {
+  FlatListPropsWithLayout,
+  runOnJS,
+  useSharedValue,
+} from 'react-native-reanimated'
 import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
 
 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
@@ -13,8 +17,8 @@ import {useTheme} from '#/alf'
 import {FlatList_INTERNAL} from './Views'
 
 export type ListMethods = FlatList_INTERNAL
-export type ListProps<ItemT> = Omit<
-  FlatListProps<ItemT>,
+export type ListProps<ItemT = any> = Omit<
+  FlatListPropsWithLayout<ItemT>,
   | 'onMomentumScrollBegin' // Use ScrollContext instead.
   | 'onMomentumScrollEnd' // Use ScrollContext instead.
   | 'onScroll' // Use ScrollContext instead.
@@ -22,6 +26,7 @@ export type ListProps<ItemT> = Omit<
   | 'onScrollEndDrag' // Use ScrollContext instead.
   | 'refreshControl' // Pass refreshing and/or onRefresh instead.
   | 'contentOffset' // Pass headerOffset instead.
+  | 'progressViewOffset' // Can't be an animated value
 > & {
   onScrolledDownChange?: (isScrolledDown: boolean) => void
   headerOffset?: number
@@ -32,130 +37,137 @@ export type ListProps<ItemT> = Omit<
   // Web only prop to contain the scroll to the container rather than the window
   disableFullWindowScroll?: boolean
   sideBorders?: boolean
+  progressViewOffset?: number
 }
 export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
 
 const SCROLLED_DOWN_LIMIT = 200
 
-function ListImpl<ItemT>(
-  {
-    onScrolledDownChange,
-    refreshing,
-    onRefresh,
-    onItemSeen,
-    headerOffset,
-    style,
-    progressViewOffset,
-    ...props
-  }: ListProps<ItemT>,
-  ref: React.Ref<ListMethods>,
-) {
-  const isScrolledDown = useSharedValue(false)
-  const t = useTheme()
-  const dedupe = useDedupe(400)
-  const {activeLightbox} = useLightbox()
-
-  function handleScrolledDownChange(didScrollDown: boolean) {
-    onScrolledDownChange?.(didScrollDown)
-  }
-
-  // Intentionally destructured outside the main thread closure.
-  // See https://github.com/bluesky-social/social-app/pull/4108.
-  const {
-    onBeginDrag: onBeginDragFromContext,
-    onEndDrag: onEndDragFromContext,
-    onScroll: onScrollFromContext,
-    onMomentumEnd: onMomentumEndFromContext,
-  } = useScrollHandlers()
-  const scrollHandler = useAnimatedScrollHandler({
-    onBeginDrag(e, ctx) {
-      onBeginDragFromContext?.(e, ctx)
+let List = React.forwardRef<ListMethods, ListProps>(
+  (
+    {
+      onScrolledDownChange,
+      refreshing,
+      onRefresh,
+      onItemSeen,
+      headerOffset,
+      style,
+      progressViewOffset,
+      ...props
     },
-    onEndDrag(e, ctx) {
-      runOnJS(updateActiveVideoViewAsync)()
-      onEndDragFromContext?.(e, ctx)
-    },
-    onScroll(e, ctx) {
-      onScrollFromContext?.(e, ctx)
+    ref,
+  ): React.ReactElement => {
+    const isScrolledDown = useSharedValue(false)
+    const t = useTheme()
+    const dedupe = useDedupe(400)
+    const {activeLightbox} = useLightbox()
 
-      const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
-      if (isScrolledDown.get() !== didScrollDown) {
-        isScrolledDown.set(didScrollDown)
-        if (onScrolledDownChange != null) {
-          runOnJS(handleScrolledDownChange)(didScrollDown)
-        }
-      }
+    function handleScrolledDownChange(didScrollDown: boolean) {
+      onScrolledDownChange?.(didScrollDown)
+    }
 
-      if (isIOS) {
-        runOnJS(dedupe)(updateActiveVideoViewAsync)
-      }
-    },
-    // Note: adding onMomentumBegin here makes simulator scroll
-    // lag on Android. So either don't add it, or figure out why.
-    onMomentumEnd(e, ctx) {
-      runOnJS(updateActiveVideoViewAsync)()
-      onMomentumEndFromContext?.(e, ctx)
-    },
-  })
+    // Intentionally destructured outside the main thread closure.
+    // See https://github.com/bluesky-social/social-app/pull/4108.
+    const {
+      onBeginDrag: onBeginDragFromContext,
+      onEndDrag: onEndDragFromContext,
+      onScroll: onScrollFromContext,
+      onMomentumEnd: onMomentumEndFromContext,
+    } = useScrollHandlers()
+    const scrollHandler = useAnimatedScrollHandler({
+      onBeginDrag(e, ctx) {
+        onBeginDragFromContext?.(e, ctx)
+      },
+      onEndDrag(e, ctx) {
+        runOnJS(updateActiveVideoViewAsync)()
+        onEndDragFromContext?.(e, ctx)
+      },
+      onScroll(e, ctx) {
+        onScrollFromContext?.(e, ctx)
 
-  const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => {
-    if (!onItemSeen) {
-      return [undefined, undefined]
-    }
-    return [
-      (info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
-        for (const item of info.changed) {
-          if (item.isViewable) {
-            onItemSeen(item.item)
+        const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
+        if (isScrolledDown.get() !== didScrollDown) {
+          isScrolledDown.set(didScrollDown)
+          if (onScrolledDownChange != null) {
+            runOnJS(handleScrolledDownChange)(didScrollDown)
           }
         }
+
+        if (isIOS) {
+          runOnJS(dedupe)(updateActiveVideoViewAsync)
+        }
       },
-      {
-        itemVisiblePercentThreshold: 40,
-        minimumViewTime: 0.5e3,
+      // Note: adding onMomentumBegin here makes simulator scroll
+      // lag on Android. So either don't add it, or figure out why.
+      onMomentumEnd(e, ctx) {
+        runOnJS(updateActiveVideoViewAsync)()
+        onMomentumEndFromContext?.(e, ctx)
       },
-    ]
-  }, [onItemSeen])
+    })
 
-  let refreshControl
-  if (refreshing !== undefined || onRefresh !== undefined) {
-    refreshControl = (
-      <RefreshControl
-        refreshing={refreshing ?? false}
-        onRefresh={onRefresh}
-        tintColor={t.atoms.text.color}
-        titleColor={t.atoms.text.color}
-        progressViewOffset={progressViewOffset ?? headerOffset}
-      />
-    )
-  }
+    const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => {
+      if (!onItemSeen) {
+        return [undefined, undefined]
+      }
+      return [
+        (info: {
+          viewableItems: Array<ViewToken>
+          changed: Array<ViewToken>
+        }) => {
+          for (const item of info.changed) {
+            if (item.isViewable) {
+              onItemSeen(item.item)
+            }
+          }
+        },
+        {
+          itemVisiblePercentThreshold: 40,
+          minimumViewTime: 0.5e3,
+        },
+      ]
+    }, [onItemSeen])
 
-  let contentOffset
-  if (headerOffset != null) {
-    style = addStyle(style, {
-      paddingTop: headerOffset,
-    })
-    contentOffset = {x: 0, y: headerOffset * -1}
-  }
+    let refreshControl
+    if (refreshing !== undefined || onRefresh !== undefined) {
+      refreshControl = (
+        <RefreshControl
+          refreshing={refreshing ?? false}
+          onRefresh={onRefresh}
+          tintColor={t.atoms.text.color}
+          titleColor={t.atoms.text.color}
+          progressViewOffset={progressViewOffset ?? headerOffset}
+        />
+      )
+    }
 
-  return (
-    <FlatList_INTERNAL
-      {...props}
-      scrollIndicatorInsets={{right: 1}}
-      contentOffset={contentOffset}
-      refreshControl={refreshControl}
-      onScroll={scrollHandler}
-      scrollsToTop={!activeLightbox}
-      scrollEventThrottle={1}
-      onViewableItemsChanged={onViewableItemsChanged}
-      viewabilityConfig={viewabilityConfig}
-      showsVerticalScrollIndicator={!isAndroid}
-      style={style}
-      ref={ref}
-    />
-  )
-}
+    let contentOffset
+    if (headerOffset != null) {
+      style = addStyle(style, {
+        paddingTop: headerOffset,
+      })
+      contentOffset = {x: 0, y: headerOffset * -1}
+    }
+
+    return (
+      <FlatList_INTERNAL
+        {...props}
+        scrollIndicatorInsets={{right: 1}}
+        contentOffset={contentOffset}
+        refreshControl={refreshControl}
+        onScroll={scrollHandler}
+        scrollsToTop={!activeLightbox}
+        scrollEventThrottle={1}
+        onViewableItemsChanged={onViewableItemsChanged}
+        viewabilityConfig={viewabilityConfig}
+        showsVerticalScrollIndicator={!isAndroid}
+        style={style}
+        // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
+        ref={ref}
+      />
+    )
+  },
+)
+List.displayName = 'List'
 
-export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
-  props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
-) => React.ReactElement
+List = memo(List)
+export {List}
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index f8ba8f561..b5075707a 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -113,6 +113,7 @@ export const ViewSelector = React.forwardRef<
   )
   return (
     <FlatList_INTERNAL
+      // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
       ref={flatListRef}
       data={data}
       keyExtractor={keyExtractor}
diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts
deleted file mode 100644
index 3f4905574..000000000
--- a/src/view/com/util/Views.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react'
-import {ViewProps} from 'react-native'
-export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native'
-export function CenteredView({
-  style,
-  sideBorders,
-  ...props
-}: React.PropsWithChildren<
-  ViewProps & {
-    /**
-     * @platform web
-     */
-    sideBorders?: boolean
-    /**
-     * @platform web
-     */
-    topBorder?: boolean
-  }
->)
diff --git a/src/view/com/util/Views.jsx b/src/view/com/util/Views.jsx
deleted file mode 100644
index 02a2b407e..000000000
--- a/src/view/com/util/Views.jsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import {View} from 'react-native'
-import Animated from 'react-native-reanimated'
-
-// If you explode these into functions, don't forget to forwardRef!
-export const FlatList_INTERNAL = Animated.FlatList
-export const CenteredView = View
-export const ScrollView = Animated.ScrollView
diff --git a/src/view/com/util/Views.tsx b/src/view/com/util/Views.tsx
new file mode 100644
index 000000000..0d3f63794
--- /dev/null
+++ b/src/view/com/util/Views.tsx
@@ -0,0 +1,28 @@
+import {forwardRef} from 'react'
+import {FlatListComponent} from 'react-native'
+import {View, ViewProps} from 'react-native'
+import Animated from 'react-native-reanimated'
+import {FlatListPropsWithLayout} from 'react-native-reanimated'
+
+// If you explode these into functions, don't forget to forwardRef!
+
+/**
+ * Avoid using `FlatList_INTERNAL` and use `List` where possible.
+ * The types are a bit wrong on `FlatList_INTERNAL`
+ */
+export const FlatList_INTERNAL = Animated.FlatList
+export type FlatList_INTERNAL<ItemT = any> = Omit<
+  FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>,
+  'CellRendererComponent'
+>
+export const ScrollView = Animated.ScrollView
+export type ScrollView = typeof Animated.ScrollView
+
+export const CenteredView = forwardRef<
+  View,
+  React.PropsWithChildren<
+    ViewProps & {sideBorders?: boolean; topBorder?: boolean}
+  >
+>(function CenteredView(props, ref) {
+  return <View ref={ref} {...props} />
+})
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 404145714..406f11792 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {ActivityIndicator, type FlatList, StyleSheet, View} from 'react-native'
+import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {AppBskyFeedDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -25,7 +25,7 @@ import {useComposerControls} from '#/state/shell/composer'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {FAB} from '#/view/com/util/fab/FAB'
 import {TextLink} from '#/view/com/util/Link'
-import {List} from '#/view/com/util/List'
+import {List, ListMethods} from '#/view/com/util/List'
 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {Text} from '#/view/com/util/text/Text'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
@@ -130,7 +130,7 @@ export function FeedsScreen(_props: Props) {
     error: searchError,
   } = useSearchPopularFeedsMutation()
   const {hasSession} = useSession()
-  const listRef = React.useRef<FlatList>(null)
+  const listRef = React.useRef<ListMethods>(null)
 
   /**
    * A search query is present. We may not have search results yet.
diff --git a/src/view/screens/Storybook/ListContained.tsx b/src/view/screens/Storybook/ListContained.tsx
index 833320148..e673743eb 100644
--- a/src/view/screens/Storybook/ListContained.tsx
+++ b/src/view/screens/Storybook/ListContained.tsx
@@ -1,15 +1,15 @@
 import React from 'react'
-import {FlatList, View} from 'react-native'
+import {View} from 'react-native'
 
 import {ScrollProvider} from '#/lib/ScrollContext'
-import {List} from '#/view/com/util/List'
+import {List, ListMethods} from '#/view/com/util/List'
 import {Button, ButtonText} from '#/components/Button'
 import * as Toggle from '#/components/forms/Toggle'
 import {Text} from '#/components/Typography'
 
 export function ListContained() {
   const [animated, setAnimated] = React.useState(false)
-  const ref = React.useRef<FlatList>(null)
+  const ref = React.useRef<ListMethods>(null)
 
   const data = React.useMemo(() => {
     return Array.from({length: 100}, (_, i) => ({