From 3e04dc7291d22a829b95ac2db6c82f9d6058f989 Mon Sep 17 00:00:00 2001 From: Whitewater Date: Sun, 3 Aug 2025 13:56:39 +0800 Subject: [PATCH] refactor: upgrade FlashList to v2 (#4298) * refactor: upgrade FlashList to v2 * chore: upgrade @shopify/flash-list to v2.0.0 * chore: update packageManager version to pnpm@10.14.0 * fix: revert packageManager version to pnpm@10.12.1 * chore: update ref type for TimelineSelectorMasonryList component --- apps/mobile/package.json | 2 +- .../src/modules/discover/Recommendations.tsx | 9 +++-- .../entry-list/EntryListContentArticle.tsx | 5 ++- .../entry-list/EntryListContentPicture.tsx | 7 ++-- .../entry-list/EntryListContentSocial.tsx | 6 ++-- .../entry-list/EntryListContentVideo.tsx | 7 ++-- .../modules/entry-list/EntryListSelector.tsx | 6 ++-- apps/mobile/src/modules/entry-list/hooks.ts | 16 ++++----- .../modules/screen/TimelineSelectorList.tsx | 35 +++++++++---------- .../subscription/SubscriptionLists.tsx | 5 ++- pnpm-lock.yaml | 31 +++------------- 11 files changed, 50 insertions(+), 79 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 17904a10d..0369461ee 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -45,7 +45,7 @@ "@react-native-masked-view/masked-view": "0.3.2", "@react-native-menu/menu": "1.2.3", "@react-native-picker/picker": "2.11.1", - "@shopify/flash-list": "1.8.3", + "@shopify/flash-list": "2.0.0", "@tanstack/query-sync-storage-persister": "5.81.5", "@tanstack/react-query": "5.81.5", "@tanstack/react-query-persist-client": "5.81.5", diff --git a/apps/mobile/src/modules/discover/Recommendations.tsx b/apps/mobile/src/modules/discover/Recommendations.tsx index 33509c038..cc8d76fd5 100644 --- a/apps/mobile/src/modules/discover/Recommendations.tsx +++ b/apps/mobile/src/modules/discover/Recommendations.tsx @@ -1,12 +1,12 @@ import { RSSHubCategories } from "@follow/constants" import type { RSSHubRouteDeclaration } from "@follow/models/rsshub" import { isASCII } from "@follow/utils" -import type { ContentStyle } from "@shopify/flash-list" +import type { FlashListRef } from "@shopify/flash-list" import { FlashList } from "@shopify/flash-list" import { useQuery } from "@tanstack/react-query" import { useEffect, useMemo, useRef, useState } from "react" import { useTranslation } from "react-i18next" -import type { ScrollView } from "react-native" +import type { ScrollView, StyleProp, ViewStyle } from "react-native" import { Animated, useAnimatedValue, useWindowDimensions, View } from "react-native" import type { SharedValue } from "react-native-reanimated" import { useSharedValue } from "react-native-reanimated" @@ -137,7 +137,7 @@ const LanguageMap = { cmn: "zh-CN", } as const export const RecommendationTab: TabComponent<{ - contentContainerStyle?: ContentStyle + contentContainerStyle?: StyleProp insets?: { top?: number bottom?: number @@ -231,7 +231,7 @@ export const RecommendationTab: TabComponent<{ // Add ref for FlashList const listRef = useRegisterNavigationScrollView< - FlashList<{ + FlashListRef<{ key: string data: RSSHubRouteDeclaration }> @@ -287,7 +287,6 @@ export const RecommendationTab: TabComponent<{ } }} scrollEventThrottle={16} - estimatedItemSize={150} ref={listRef} data={alphabetGroups} keyExtractor={keyExtractor} diff --git a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx index b25a21a67..97d7f42a2 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx @@ -1,6 +1,6 @@ import type { FeedViewType } from "@follow/constants" import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks" -import type { FlashList, ListRenderItemInfo } from "@shopify/flash-list" +import type { FlashListRef, ListRenderItemInfo } from "@shopify/flash-list" import type { ElementRef } from "react" import { useCallback, useImperativeHandle, useMemo, useRef } from "react" import { View } from "react-native" @@ -43,7 +43,7 @@ export const EntryListContentArticle = ({ [hasNextPage, fetchedTime], ) - const ref = useRef>(null) + const ref = useRef>(null) const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({ disabled: active === false || isFetching, @@ -82,7 +82,6 @@ export const EntryListContentArticle = ({ data={entryIds} extraData={extraData} keyExtractor={defaultKeyExtractor} - estimatedItemSize={100} renderItem={renderItem} onEndReached={fetchNextPage} onScroll={onScroll} diff --git a/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx index 5448284ce..08ccf0812 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx @@ -1,6 +1,6 @@ import { useTypeScriptHappyCallback } from "@follow/hooks" import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks" -import type { FlashList, MasonryFlashListProps } from "@shopify/flash-list" +import type { FlashListProps, FlashListRef } from "@shopify/flash-list" import type { ElementRef } from "react" import { useImperativeHandle, useRef } from "react" import { StyleSheet, View } from "react-native" @@ -24,10 +24,10 @@ export const EntryListContentPicture = ({ active, ...rest }: { entryIds: string[] | null; active?: boolean } & Omit< - MasonryFlashListProps, + FlashListProps, "data" | "renderItem" > & { ref?: React.Ref | null> }) => { - const ref = useRef>(null) + const ref = useRef>(null) useImperativeHandle(forwardRef, () => ref.current!) const { fetchNextPage, refetch, isRefetching, hasNextPage, isFetching, isReady } = useEntries() @@ -84,7 +84,6 @@ export const EntryListContentPicture = ({ onScroll={onScroll} onEndReached={fetchNextPage} numColumns={2} - estimatedItemSize={100} contentContainerStyle={styles.contentContainer} ListFooterComponent={ hasNextPage ? ( diff --git a/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx b/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx index 33d35f0b6..99639d135 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx @@ -1,5 +1,5 @@ import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks" -import type { FlashList, ListRenderItemInfo } from "@shopify/flash-list" +import type { FlashListRef, ListRenderItemInfo } from "@shopify/flash-list" import type { ElementRef } from "react" import { useCallback, useImperativeHandle, useMemo, useRef } from "react" import { View } from "react-native" @@ -25,7 +25,7 @@ export const EntryListContentSocial = ({ const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage, isReady } = useEntries() const extraData: EntryExtraData = useMemo(() => ({ entryIds }), [entryIds]) - const ref = useRef>(null) + const ref = useRef>(null) useImperativeHandle(forwardRef, () => ref.current!) // eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-callback const renderItem = useCallback( @@ -61,7 +61,6 @@ export const EntryListContentSocial = ({ isRefetching={false} data={Array.from({ length: 5 }).map((_, index) => `skeleton-${index}`)} keyExtractor={(id) => id} - estimatedItemSize={5} renderItem={EntryItemSkeleton} ItemSeparatorComponent={ItemSeparatorFullWidth} /> @@ -78,7 +77,6 @@ export const EntryListContentSocial = ({ data={entryIds} extraData={extraData} keyExtractor={(id) => id} - estimatedItemSize={100} renderItem={renderItem} onEndReached={fetchNextPage} onViewableItemsChanged={onViewableItemsChanged} diff --git a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx index f9ccbcc5c..d963abaaa 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx @@ -1,6 +1,6 @@ import { useTypeScriptHappyCallback } from "@follow/hooks" import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks" -import type { FlashList, MasonryFlashListProps } from "@shopify/flash-list" +import type { FlashListProps, FlashListRef } from "@shopify/flash-list" import type { ElementRef } from "react" import { useImperativeHandle, useMemo, useRef } from "react" import { StyleSheet, View } from "react-native" @@ -22,10 +22,10 @@ export const EntryListContentVideo = ({ active, ...rest }: { entryIds: string[] | null; active?: boolean } & Omit< - MasonryFlashListProps, + FlashListProps, "data" | "renderItem" > & { ref?: React.Ref | null> }) => { - const ref = useRef>(null) + const ref = useRef>(null) useImperativeHandle(forwardRef, () => ref.current!) const { fetchNextPage, refetch, isRefetching, isFetching, hasNextPage, isReady } = useEntries() const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({ @@ -95,7 +95,6 @@ export const EntryListContentVideo = ({ onScroll={onScroll} onEndReached={fetchNextPage} numColumns={2} - estimatedItemSize={100} ListFooterComponent={ListFooterComponent} {...rest} onRefresh={refetch} diff --git a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx index 8a19aba00..83949351d 100644 --- a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx @@ -1,6 +1,6 @@ import { FeedViewType } from "@follow/constants" import { useWhoami } from "@follow/store/user/hooks" -import type { FlashList } from "@shopify/flash-list" +import type { FlashListRef } from "@shopify/flash-list" import type { RefObject } from "react" import { useEffect } from "react" @@ -30,7 +30,7 @@ type EntryListSelectorProps = { } function EntryListSelectorImpl({ entryIds, viewId, active = true }: EntryListSelectorProps) { - const ref = useRegisterNavigationScrollView>(active) + const ref = useRegisterNavigationScrollView>(active) let ContentComponent: | typeof EntryListContentSocial @@ -89,7 +89,7 @@ export const EntryListSelector = withErrorBoundary( ) const useAutoScrollToEntryAfterPullUpToNext = ( - ref: RefObject | null>, + ref: RefObject | null>, entryIds: string[], ) => { const navigation = useNavigation() diff --git a/apps/mobile/src/modules/entry-list/hooks.ts b/apps/mobile/src/modules/entry-list/hooks.ts index 989cbaeb8..63a8d49c2 100644 --- a/apps/mobile/src/modules/entry-list/hooks.ts +++ b/apps/mobile/src/modules/entry-list/hooks.ts @@ -1,6 +1,6 @@ import { debouncedFetchEntryContentByStream } from "@follow/store/entry/store" import { unreadSyncService } from "@follow/store/unread/store" -import type ViewToken from "@shopify/flash-list/dist/viewability/ViewToken" +import type { ViewToken } from "@shopify/flash-list" import { fetch as expoFetch } from "expo/fetch" import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react" import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native" @@ -8,14 +8,14 @@ import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native" import { useGeneralSettingKey } from "@/src/atoms/settings/general" import { getCookie } from "@/src/lib/auth" -const defaultIdExtractor = (item: ViewToken) => item.key +const defaultIdExtractor = (item: ViewToken) => item.key export function useOnViewableItemsChanged({ disabled, idExtractor = defaultIdExtractor, onScroll: onScrollProp, }: { disabled?: boolean - idExtractor?: (item: ViewToken) => string + idExtractor?: (item: ViewToken) => string onScroll?: (e: NativeSyntheticEvent) => void } = {}) { const orientation = useRef<"down" | "up">("down") @@ -23,15 +23,15 @@ export function useOnViewableItemsChanged({ const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread") const markAsReadWhenRendering = useGeneralSettingKey("renderMarkUnread") - const [viewableItems, setViewableItems] = useState([]) - const [lastViewableItems, setLastViewableItems] = useState() - const [lastRemovedItems, setLastRemovedItems] = useState(null) + const [viewableItems, setViewableItems] = useState[]>([]) + const [lastViewableItems, setLastViewableItems] = useState[] | null>() + const [lastRemovedItems, setLastRemovedItems] = useState[] | null>(null) const [stableIdExtractor] = useState(() => idExtractor) const onViewableItemsChanged: (info: { - viewableItems: ViewToken[] - changed: ViewToken[] + viewableItems: ViewToken[] + changed: ViewToken[] }) => void = useNonReactiveCallback(({ viewableItems, changed }) => { setViewableItems(viewableItems) diff --git a/apps/mobile/src/modules/screen/TimelineSelectorList.tsx b/apps/mobile/src/modules/screen/TimelineSelectorList.tsx index 7c7994ae5..affac8f9e 100644 --- a/apps/mobile/src/modules/screen/TimelineSelectorList.tsx +++ b/apps/mobile/src/modules/screen/TimelineSelectorList.tsx @@ -2,14 +2,9 @@ import { useTypeScriptHappyCallback } from "@follow/hooks" import { usePrefetchSubscription } from "@follow/store/subscription/hooks" import { usePrefetchUnread } from "@follow/store/unread/hooks" import { nextFrame } from "@follow/utils" -import type { - FlashListProps, - MasonryFlashListProps, - MasonryFlashListRef, -} from "@shopify/flash-list" -import { FlashList, MasonryFlashList } from "@shopify/flash-list" +import type { FlashListProps, FlashListRef } from "@shopify/flash-list" +import { FlashList } from "@shopify/flash-list" import * as Haptics from "expo-haptics" -import type { ElementRef, RefObject } from "react" import { use, useCallback, useImperativeHandle, useRef } from "react" import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native" import { RefreshControl, View } from "react-native" @@ -32,8 +27,9 @@ export const TimelineSelectorList = ({ onRefresh, isRefetching, ...props -}: Props & Omit, "onRefresh"> & { ref?: React.Ref | null> }) => { - const ref = useRef>(null) +}: Props & + Omit, "onRefresh"> & { ref?: React.Ref | null> }) => { + const ref = useRef>(null) useImperativeHandle(forwardedRef, () => ref.current!) const { refetch: unreadRefetch } = usePrefetchUnread() const { refetch: subscriptionRefetch } = usePrefetchSubscription() @@ -118,8 +114,8 @@ export const TimelineSelectorMasonryList = ({ isRefetching, ...props }: Props & - Omit, "onRefresh"> & { - ref?: React.Ref | null> + Omit, "onRefresh"> & { + ref?: React.Ref | null> }) => { const { refetch: unreadRefetch } = usePrefetchUnread() const { refetch: subscriptionRefetch } = usePrefetchSubscription() @@ -147,8 +143,9 @@ export const TimelineSelectorMasonryList = ({ } return ( - >} + } {...props} - contentContainerStyle={{ - paddingTop: headerHeight, - paddingBottom: tabBarHeight, - ...props.contentContainerStyle, - }} + contentContainerStyle={[ + { + paddingTop: headerHeight, + paddingBottom: tabBarHeight, + }, + props.contentContainerStyle, + ]} scrollIndicatorInsets={{ top: headerHeight - insets.top, bottom: tabBarHeight ? tabBarHeight - insets.bottom : undefined, diff --git a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx index ac5cafafe..c66ea546a 100644 --- a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx +++ b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx @@ -9,7 +9,7 @@ import { useSortedUngroupedSubscription, } from "@follow/store/subscription/hooks" import { subscriptionSyncService } from "@follow/store/subscription/store" -import type { FlashList } from "@shopify/flash-list" +import type { FlashListRef } from "@shopify/flash-list" import type { ParseKeys } from "i18next" import { memo, useCallback, useMemo, useState } from "react" import { useTranslation } from "react-i18next" @@ -127,7 +127,7 @@ const SubscriptionListImpl = ({ const onRefresh = useEventCallback(() => { return subscriptionSyncService.fetch(view) }) - const scrollViewRef = useRegisterNavigationScrollView | null>(active) + const scrollViewRef = useRegisterNavigationScrollView | null>(active) return ( = 10.13.0'} - recyclerlistview@4.2.3: - resolution: {integrity: sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==} - peerDependencies: - react: 19.0.0 - react-native: '>= 0.30.0' - redeyed@2.1.1: resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==} @@ -15151,9 +15145,6 @@ packages: '@swc/wasm': optional: true - ts-object-utils@0.0.5: - resolution: {integrity: sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==} - ts-pattern@5.7.1: resolution: {integrity: sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==} @@ -22090,13 +22081,11 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@shopify/flash-list@1.8.3(@babel/runtime@7.27.6)(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)': + '@shopify/flash-list@2.0.0(@babel/runtime@7.27.6)(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.27.6 react: 19.0.0 react-native: 0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0) - recyclerlistview: 4.2.3(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0) - tslib: 2.8.1 '@shuding/opentype.js@1.4.0-beta.0': dependencies: @@ -31236,14 +31225,6 @@ snapshots: dependencies: resolve: 1.22.10 - recyclerlistview@4.2.3(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0): - dependencies: - lodash.debounce: 4.0.8 - prop-types: 15.8.1 - react: 19.0.0 - react-native: 0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0) - ts-object-utils: 0.0.5 - redeyed@2.1.1: dependencies: esprima: 4.0.1 @@ -32645,8 +32626,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-object-utils@0.0.5: {} - ts-pattern@5.7.1: {} ts-tiny-invariant@2.0.5: {}