diff options
-rw-r--r-- | src/screens/Messages/components/MessagesList.tsx | 6 | ||||
-rw-r--r-- | src/screens/Onboarding/Layout.tsx | 3 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.web.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/List.tsx | 240 | ||||
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 1 | ||||
-rw-r--r-- | src/view/com/util/Views.d.ts | 19 | ||||
-rw-r--r-- | src/view/com/util/Views.jsx | 7 | ||||
-rw-r--r-- | src/view/com/util/Views.tsx | 28 | ||||
-rw-r--r-- | src/view/screens/Feeds.tsx | 6 | ||||
-rw-r--r-- | src/view/screens/Storybook/ListContained.tsx | 6 |
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) => ({ |