my fork of the bluesky client

Fix `<List>` types (#6756)

* fix List and ScrollView types

* add comment

* rm omitting ref

authored by samuel.fm and committed by GitHub ce1b04b9 56a88098

Changed files
+168 -152
src
screens
Messages
components
Onboarding
view
+3 -3
src/screens/Messages/components/MessagesList.tsx
··· 1 1 import React, {useCallback, useRef} from 'react' 2 - import {FlatList, LayoutChangeEvent, View} from 'react-native' 2 + import {LayoutChangeEvent, View} from 'react-native' 3 3 import { 4 4 KeyboardStickyView, 5 5 useKeyboardHandler, ··· 33 33 EmojiPicker, 34 34 EmojiPickerState, 35 35 } from '#/view/com/composer/text-input/web/EmojiPicker.web' 36 - import {List} from '#/view/com/util/List' 36 + import {List, ListMethods} from '#/view/com/util/List' 37 37 import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled' 38 38 import {MessageInput} from '#/screens/Messages/components/MessageInput' 39 39 import {MessageListError} from '#/screens/Messages/components/MessageListError' ··· 94 94 const getPost = useGetPost() 95 95 const {embedUri, setEmbed} = useMessageEmbed() 96 96 97 - const flatListRef = useAnimatedRef<FlatList>() 97 + const flatListRef = useAnimatedRef<ListMethods>() 98 98 99 99 const [newMessagesPill, setNewMessagesPill] = React.useState({ 100 100 show: false,
+2 -1
src/screens/Onboarding/Layout.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 + import Animated from 'react-native-reanimated' 3 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 5 import {msg} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' ··· 35 36 const {gtMobile} = useBreakpoints() 36 37 const onboardDispatch = useOnboardingDispatch() 37 38 const {state, dispatch} = React.useContext(Context) 38 - const scrollview = React.useRef<ScrollView>(null) 39 + const scrollview = React.useRef<Animated.ScrollView>(null) 39 40 const prevActiveStep = React.useRef<string>(state.activeStep) 40 41 41 42 React.useEffect(() => {
+2 -2
src/view/com/pager/PagerWithHeader.web.tsx
··· 1 1 import * as React from 'react' 2 - import {FlatList, ScrollView, StyleSheet, View} from 'react-native' 2 + import {ScrollView, StyleSheet, View} from 'react-native' 3 3 import {useAnimatedRef} from 'react-native-reanimated' 4 4 5 5 import {usePalette} from '#/lib/hooks/usePalette' ··· 11 11 export interface PagerWithHeaderChildParams { 12 12 headerHeight: number 13 13 isFocused: boolean 14 - scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null> 14 + scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null> 15 15 } 16 16 17 17 export interface PagerWithHeaderProps {
+126 -114
src/view/com/util/List.tsx
··· 1 1 import React, {memo} from 'react' 2 - import {FlatListProps, RefreshControl, ViewToken} from 'react-native' 3 - import {runOnJS, useSharedValue} from 'react-native-reanimated' 2 + import {RefreshControl, ViewToken} from 'react-native' 3 + import { 4 + FlatListPropsWithLayout, 5 + runOnJS, 6 + useSharedValue, 7 + } from 'react-native-reanimated' 4 8 import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video' 5 9 6 10 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' ··· 13 17 import {FlatList_INTERNAL} from './Views' 14 18 15 19 export type ListMethods = FlatList_INTERNAL 16 - export type ListProps<ItemT> = Omit< 17 - FlatListProps<ItemT>, 20 + export type ListProps<ItemT = any> = Omit< 21 + FlatListPropsWithLayout<ItemT>, 18 22 | 'onMomentumScrollBegin' // Use ScrollContext instead. 19 23 | 'onMomentumScrollEnd' // Use ScrollContext instead. 20 24 | 'onScroll' // Use ScrollContext instead. ··· 22 26 | 'onScrollEndDrag' // Use ScrollContext instead. 23 27 | 'refreshControl' // Pass refreshing and/or onRefresh instead. 24 28 | 'contentOffset' // Pass headerOffset instead. 29 + | 'progressViewOffset' // Can't be an animated value 25 30 > & { 26 31 onScrolledDownChange?: (isScrolledDown: boolean) => void 27 32 headerOffset?: number ··· 32 37 // Web only prop to contain the scroll to the container rather than the window 33 38 disableFullWindowScroll?: boolean 34 39 sideBorders?: boolean 40 + progressViewOffset?: number 35 41 } 36 42 export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null> 37 43 38 44 const SCROLLED_DOWN_LIMIT = 200 39 45 40 - function ListImpl<ItemT>( 41 - { 42 - onScrolledDownChange, 43 - refreshing, 44 - onRefresh, 45 - onItemSeen, 46 - headerOffset, 47 - style, 48 - progressViewOffset, 49 - ...props 50 - }: ListProps<ItemT>, 51 - ref: React.Ref<ListMethods>, 52 - ) { 53 - const isScrolledDown = useSharedValue(false) 54 - const t = useTheme() 55 - const dedupe = useDedupe(400) 56 - const {activeLightbox} = useLightbox() 57 - 58 - function handleScrolledDownChange(didScrollDown: boolean) { 59 - onScrolledDownChange?.(didScrollDown) 60 - } 61 - 62 - // Intentionally destructured outside the main thread closure. 63 - // See https://github.com/bluesky-social/social-app/pull/4108. 64 - const { 65 - onBeginDrag: onBeginDragFromContext, 66 - onEndDrag: onEndDragFromContext, 67 - onScroll: onScrollFromContext, 68 - onMomentumEnd: onMomentumEndFromContext, 69 - } = useScrollHandlers() 70 - const scrollHandler = useAnimatedScrollHandler({ 71 - onBeginDrag(e, ctx) { 72 - onBeginDragFromContext?.(e, ctx) 73 - }, 74 - onEndDrag(e, ctx) { 75 - runOnJS(updateActiveVideoViewAsync)() 76 - onEndDragFromContext?.(e, ctx) 46 + let List = React.forwardRef<ListMethods, ListProps>( 47 + ( 48 + { 49 + onScrolledDownChange, 50 + refreshing, 51 + onRefresh, 52 + onItemSeen, 53 + headerOffset, 54 + style, 55 + progressViewOffset, 56 + ...props 77 57 }, 78 - onScroll(e, ctx) { 79 - onScrollFromContext?.(e, ctx) 58 + ref, 59 + ): React.ReactElement => { 60 + const isScrolledDown = useSharedValue(false) 61 + const t = useTheme() 62 + const dedupe = useDedupe(400) 63 + const {activeLightbox} = useLightbox() 80 64 81 - const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT 82 - if (isScrolledDown.get() !== didScrollDown) { 83 - isScrolledDown.set(didScrollDown) 84 - if (onScrolledDownChange != null) { 85 - runOnJS(handleScrolledDownChange)(didScrollDown) 86 - } 87 - } 65 + function handleScrolledDownChange(didScrollDown: boolean) { 66 + onScrolledDownChange?.(didScrollDown) 67 + } 88 68 89 - if (isIOS) { 90 - runOnJS(dedupe)(updateActiveVideoViewAsync) 91 - } 92 - }, 93 - // Note: adding onMomentumBegin here makes simulator scroll 94 - // lag on Android. So either don't add it, or figure out why. 95 - onMomentumEnd(e, ctx) { 96 - runOnJS(updateActiveVideoViewAsync)() 97 - onMomentumEndFromContext?.(e, ctx) 98 - }, 99 - }) 69 + // Intentionally destructured outside the main thread closure. 70 + // See https://github.com/bluesky-social/social-app/pull/4108. 71 + const { 72 + onBeginDrag: onBeginDragFromContext, 73 + onEndDrag: onEndDragFromContext, 74 + onScroll: onScrollFromContext, 75 + onMomentumEnd: onMomentumEndFromContext, 76 + } = useScrollHandlers() 77 + const scrollHandler = useAnimatedScrollHandler({ 78 + onBeginDrag(e, ctx) { 79 + onBeginDragFromContext?.(e, ctx) 80 + }, 81 + onEndDrag(e, ctx) { 82 + runOnJS(updateActiveVideoViewAsync)() 83 + onEndDragFromContext?.(e, ctx) 84 + }, 85 + onScroll(e, ctx) { 86 + onScrollFromContext?.(e, ctx) 100 87 101 - const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => { 102 - if (!onItemSeen) { 103 - return [undefined, undefined] 104 - } 105 - return [ 106 - (info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => { 107 - for (const item of info.changed) { 108 - if (item.isViewable) { 109 - onItemSeen(item.item) 88 + const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT 89 + if (isScrolledDown.get() !== didScrollDown) { 90 + isScrolledDown.set(didScrollDown) 91 + if (onScrolledDownChange != null) { 92 + runOnJS(handleScrolledDownChange)(didScrollDown) 110 93 } 111 94 } 95 + 96 + if (isIOS) { 97 + runOnJS(dedupe)(updateActiveVideoViewAsync) 98 + } 112 99 }, 113 - { 114 - itemVisiblePercentThreshold: 40, 115 - minimumViewTime: 0.5e3, 100 + // Note: adding onMomentumBegin here makes simulator scroll 101 + // lag on Android. So either don't add it, or figure out why. 102 + onMomentumEnd(e, ctx) { 103 + runOnJS(updateActiveVideoViewAsync)() 104 + onMomentumEndFromContext?.(e, ctx) 116 105 }, 117 - ] 118 - }, [onItemSeen]) 106 + }) 119 107 120 - let refreshControl 121 - if (refreshing !== undefined || onRefresh !== undefined) { 122 - refreshControl = ( 123 - <RefreshControl 124 - refreshing={refreshing ?? false} 125 - onRefresh={onRefresh} 126 - tintColor={t.atoms.text.color} 127 - titleColor={t.atoms.text.color} 128 - progressViewOffset={progressViewOffset ?? headerOffset} 129 - /> 130 - ) 131 - } 108 + const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => { 109 + if (!onItemSeen) { 110 + return [undefined, undefined] 111 + } 112 + return [ 113 + (info: { 114 + viewableItems: Array<ViewToken> 115 + changed: Array<ViewToken> 116 + }) => { 117 + for (const item of info.changed) { 118 + if (item.isViewable) { 119 + onItemSeen(item.item) 120 + } 121 + } 122 + }, 123 + { 124 + itemVisiblePercentThreshold: 40, 125 + minimumViewTime: 0.5e3, 126 + }, 127 + ] 128 + }, [onItemSeen]) 132 129 133 - let contentOffset 134 - if (headerOffset != null) { 135 - style = addStyle(style, { 136 - paddingTop: headerOffset, 137 - }) 138 - contentOffset = {x: 0, y: headerOffset * -1} 139 - } 130 + let refreshControl 131 + if (refreshing !== undefined || onRefresh !== undefined) { 132 + refreshControl = ( 133 + <RefreshControl 134 + refreshing={refreshing ?? false} 135 + onRefresh={onRefresh} 136 + tintColor={t.atoms.text.color} 137 + titleColor={t.atoms.text.color} 138 + progressViewOffset={progressViewOffset ?? headerOffset} 139 + /> 140 + ) 141 + } 140 142 141 - return ( 142 - <FlatList_INTERNAL 143 - {...props} 144 - scrollIndicatorInsets={{right: 1}} 145 - contentOffset={contentOffset} 146 - refreshControl={refreshControl} 147 - onScroll={scrollHandler} 148 - scrollsToTop={!activeLightbox} 149 - scrollEventThrottle={1} 150 - onViewableItemsChanged={onViewableItemsChanged} 151 - viewabilityConfig={viewabilityConfig} 152 - showsVerticalScrollIndicator={!isAndroid} 153 - style={style} 154 - ref={ref} 155 - /> 156 - ) 157 - } 143 + let contentOffset 144 + if (headerOffset != null) { 145 + style = addStyle(style, { 146 + paddingTop: headerOffset, 147 + }) 148 + contentOffset = {x: 0, y: headerOffset * -1} 149 + } 158 150 159 - export const List = memo(React.forwardRef(ListImpl)) as <ItemT>( 160 - props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>}, 161 - ) => React.ReactElement 151 + return ( 152 + <FlatList_INTERNAL 153 + {...props} 154 + scrollIndicatorInsets={{right: 1}} 155 + contentOffset={contentOffset} 156 + refreshControl={refreshControl} 157 + onScroll={scrollHandler} 158 + scrollsToTop={!activeLightbox} 159 + scrollEventThrottle={1} 160 + onViewableItemsChanged={onViewableItemsChanged} 161 + viewabilityConfig={viewabilityConfig} 162 + showsVerticalScrollIndicator={!isAndroid} 163 + style={style} 164 + // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn 165 + ref={ref} 166 + /> 167 + ) 168 + }, 169 + ) 170 + List.displayName = 'List' 171 + 172 + List = memo(List) 173 + export {List}
+1
src/view/com/util/ViewSelector.tsx
··· 113 113 ) 114 114 return ( 115 115 <FlatList_INTERNAL 116 + // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn 116 117 ref={flatListRef} 117 118 data={data} 118 119 keyExtractor={keyExtractor}
-19
src/view/com/util/Views.d.ts
··· 1 - import React from 'react' 2 - import {ViewProps} from 'react-native' 3 - export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native' 4 - export function CenteredView({ 5 - style, 6 - sideBorders, 7 - ...props 8 - }: React.PropsWithChildren< 9 - ViewProps & { 10 - /** 11 - * @platform web 12 - */ 13 - sideBorders?: boolean 14 - /** 15 - * @platform web 16 - */ 17 - topBorder?: boolean 18 - } 19 - >)
-7
src/view/com/util/Views.jsx
··· 1 - import {View} from 'react-native' 2 - import Animated from 'react-native-reanimated' 3 - 4 - // If you explode these into functions, don't forget to forwardRef! 5 - export const FlatList_INTERNAL = Animated.FlatList 6 - export const CenteredView = View 7 - export const ScrollView = Animated.ScrollView
+28
src/view/com/util/Views.tsx
··· 1 + import {forwardRef} from 'react' 2 + import {FlatListComponent} from 'react-native' 3 + import {View, ViewProps} from 'react-native' 4 + import Animated from 'react-native-reanimated' 5 + import {FlatListPropsWithLayout} from 'react-native-reanimated' 6 + 7 + // If you explode these into functions, don't forget to forwardRef! 8 + 9 + /** 10 + * Avoid using `FlatList_INTERNAL` and use `List` where possible. 11 + * The types are a bit wrong on `FlatList_INTERNAL` 12 + */ 13 + export const FlatList_INTERNAL = Animated.FlatList 14 + export type FlatList_INTERNAL<ItemT = any> = Omit< 15 + FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>, 16 + 'CellRendererComponent' 17 + > 18 + export const ScrollView = Animated.ScrollView 19 + export type ScrollView = typeof Animated.ScrollView 20 + 21 + export const CenteredView = forwardRef< 22 + View, 23 + React.PropsWithChildren< 24 + ViewProps & {sideBorders?: boolean; topBorder?: boolean} 25 + > 26 + >(function CenteredView(props, ref) { 27 + return <View ref={ref} {...props} /> 28 + })
+3 -3
src/view/screens/Feeds.tsx
··· 1 1 import React from 'react' 2 - import {ActivityIndicator, type FlatList, StyleSheet, View} from 'react-native' 2 + import {ActivityIndicator, StyleSheet, View} from 'react-native' 3 3 import {AppBskyFeedDefs} from '@atproto/api' 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' ··· 25 25 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 26 26 import {FAB} from '#/view/com/util/fab/FAB' 27 27 import {TextLink} from '#/view/com/util/Link' 28 - import {List} from '#/view/com/util/List' 28 + import {List, ListMethods} from '#/view/com/util/List' 29 29 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 30 30 import {Text} from '#/view/com/util/text/Text' 31 31 import {ViewHeader} from '#/view/com/util/ViewHeader' ··· 130 130 error: searchError, 131 131 } = useSearchPopularFeedsMutation() 132 132 const {hasSession} = useSession() 133 - const listRef = React.useRef<FlatList>(null) 133 + const listRef = React.useRef<ListMethods>(null) 134 134 135 135 /** 136 136 * A search query is present. We may not have search results yet.
+3 -3
src/view/screens/Storybook/ListContained.tsx
··· 1 1 import React from 'react' 2 - import {FlatList, View} from 'react-native' 2 + import {View} from 'react-native' 3 3 4 4 import {ScrollProvider} from '#/lib/ScrollContext' 5 - import {List} from '#/view/com/util/List' 5 + import {List, ListMethods} from '#/view/com/util/List' 6 6 import {Button, ButtonText} from '#/components/Button' 7 7 import * as Toggle from '#/components/forms/Toggle' 8 8 import {Text} from '#/components/Typography' 9 9 10 10 export function ListContained() { 11 11 const [animated, setAnimated] = React.useState(false) 12 - const ref = React.useRef<FlatList>(null) 12 + const ref = React.useRef<ListMethods>(null) 13 13 14 14 const data = React.useMemo(() => { 15 15 return Array.from({length: 100}, (_, i) => ({