Skip to content

Commit

Permalink
refactor(mobile): improve scroll-to-top handling (#2910)
Browse files Browse the repository at this point in the history
* refactor(mobile): enhance navigation scroll view handling and type safety

* fix(mobile): update contentScrollerRef in discover screen

* fix(mobile): integrate navigation listener for scroll view reference attachment

* fix: types
  • Loading branch information
lawvs authored Feb 28, 2025
1 parent 30aef70 commit 34540a1
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 34 deletions.
25 changes: 16 additions & 9 deletions apps/mobile/src/components/layouts/tabbar/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useNavigation } from "expo-router"
import type { RefObject } from "react"
import { useCallback, useContext, useEffect, useRef } from "react"
import type { FlatList, ScrollView } from "react-native"
Expand All @@ -6,8 +7,8 @@ import { findNodeHandle, Platform } from "react-native"
import { performNativeScrollToTop } from "@/src/lib/native"

import {
AttachNavigationScrollViewContext,
SetAttachNavigationScrollViewContext,
useAttachNavigationScrollView,
} from "./contexts/AttachNavigationScrollViewContext"
import { BottomTabBarHeightContext } from "./contexts/BottomTabBarHeightContext"

Expand All @@ -19,7 +20,7 @@ export const useBottomTabBarHeight = () => {
export const useNavigationScrollToTop = (
overrideScrollerRef?: React.RefObject<ScrollView> | React.RefObject<FlatList<any>> | null,
) => {
const attachNavigationScrollViewRef = useContext(AttachNavigationScrollViewContext)
const attachNavigationScrollViewRef = useAttachNavigationScrollView()
return useCallback(() => {
const $scroller = overrideScrollerRef?.current ?? attachNavigationScrollViewRef?.current
if (!$scroller) return
Expand All @@ -33,17 +34,17 @@ export const useNavigationScrollToTop = (
}

if ("scrollTo" in $scroller) {
;($scroller as ScrollView).scrollTo({
void ($scroller as ScrollView).scrollTo({
y: 0,
animated: true,
})
} else if ("scrollToIndex" in $scroller) {
;($scroller as FlatList<any>).scrollToIndex({
void ($scroller as FlatList<any>).scrollToIndex({
index: 0,
animated: true,
})
} else if ("scrollToOffset" in $scroller) {
;($scroller as FlatList<any>).scrollToOffset({
void ($scroller as FlatList<any>).scrollToOffset({
offset: 0,
animated: true,
})
Expand All @@ -52,13 +53,19 @@ export const useNavigationScrollToTop = (
}, [attachNavigationScrollViewRef, overrideScrollerRef])
}

export const useRegisterNavigationScrollView = <T = any>() => {
export const useRegisterNavigationScrollView = <T = unknown>(active = true) => {
const scrollViewRef = useRef<T>(null)
const navigation = useNavigation()
const setAttachNavigationScrollViewRef = useContext(SetAttachNavigationScrollViewContext)
useEffect(() => {
if (setAttachNavigationScrollViewRef) {
if (!active) return
if (!setAttachNavigationScrollViewRef) return

setAttachNavigationScrollViewRef(scrollViewRef as unknown as RefObject<ScrollView>)
const unsubscribe = navigation.addListener("focus", () => {
setAttachNavigationScrollViewRef(scrollViewRef as unknown as RefObject<ScrollView>)
}
}, [setAttachNavigationScrollViewRef, scrollViewRef])
})
return unsubscribe
}, [setAttachNavigationScrollViewRef, scrollViewRef, active, navigation])
return scrollViewRef
}
13 changes: 8 additions & 5 deletions apps/mobile/src/modules/discover/Recommendations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ export const Recommendations = () => {
const currentTab = useAtomValue(currentTabAtom)

const windowWidth = useWindowDimensions().width
const contentScrollerRef = useRegisterNavigationScrollView<ScrollView>()
const ref = useRef<ScrollView>(null)

useEffect(() => {
contentScrollerRef.current?.scrollTo({ x: currentTab * windowWidth, y: 0, animated: true })
}, [contentScrollerRef, currentTab, windowWidth])
ref.current?.scrollTo({ x: currentTab * windowWidth, y: 0, animated: true })
}, [ref, currentTab, windowWidth])

const [loadedTabIndex, setLoadedTabIndex] = useState(() => new Set())
useEffect(() => {
Expand All @@ -55,7 +55,7 @@ export const Recommendations = () => {
onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: animatedX } } }], {
useNativeDriver: true,
})}
ref={contentScrollerRef}
ref={ref}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
Expand Down Expand Up @@ -176,7 +176,10 @@ const Tab: TabComponent = ({ tab, isSelected, ...rest }) => {
}, [data, keys])

// Add ref for FlashList
const listRef = useRef<FlashList<{ key: string; data: RSSHubRouteDeclaration } | string>>(null)
const listRef =
useRegisterNavigationScrollView<
FlashList<{ key: string; data: RSSHubRouteDeclaration } | string>
>(isSelected)

const getItemType = useCallback((item: string | { key: string }) => {
return typeof item === "string" ? "sectionHeader" : "row"
Expand Down
15 changes: 2 additions & 13 deletions apps/mobile/src/modules/entry-list/EntryListSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { FeedViewType } from "@follow/constants"
import type { FlashList } from "@shopify/flash-list"
import type { RefObject } from "react"
import { useContext, useEffect, useRef } from "react"
import type { ScrollView } from "react-native"

import { SetAttachNavigationScrollViewContext } from "@/src/components/layouts/tabbar/contexts/AttachNavigationScrollViewContext"
import { useRegisterNavigationScrollView } from "@/src/components/layouts/tabbar/hooks"
import { EntryListContentPicture } from "@/src/modules/entry-list/EntryListContentPicture"

import { EntryListContentArticle } from "./EntryListContentArticle"
Expand All @@ -21,15 +18,7 @@ export function EntryListSelector({
viewId: FeedViewType
active?: boolean
}) {
const setAttachNavigationScrollViewRef = useContext(SetAttachNavigationScrollViewContext)

const ref = useRef<FlashList<any>>(null)
useEffect(() => {
if (!active) return
if (setAttachNavigationScrollViewRef) {
setAttachNavigationScrollViewRef(ref as unknown as RefObject<ScrollView>)
}
}, [setAttachNavigationScrollViewRef, ref, active])
const ref = useRegisterNavigationScrollView<FlashList<any>>(active)

let ContentComponent: typeof EntryListContentSocial | typeof EntryListContentPicture =
EntryListContentArticle
Expand Down
8 changes: 4 additions & 4 deletions apps/mobile/src/modules/screen/TimelineSelectorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
} from "@shopify/flash-list"
import { FlashList, MasonryFlashList } from "@shopify/flash-list"
import type { ElementRef, RefObject } from "react"
import { forwardRef, useCallback, useContext, useImperativeHandle, useRef } from "react"
import { forwardRef, useCallback, useContext } from "react"
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
import { RefreshControl } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
Expand Down Expand Up @@ -45,15 +45,15 @@ export const TimelineSelectorList = forwardRef<

const systemFill = useColor("secondaryLabel")

const listRef = useRef<FlashList<any>>(null)
// const listRef = useRef<FlashList<any>>(null)

useImperativeHandle(ref, () => listRef.current!)
// useImperativeHandle(ref, () => listRef.current!)

return (
<FlashList
automaticallyAdjustsScrollIndicatorInsets={false}
automaticallyAdjustContentInsets={false}
ref={listRef}
ref={ref}
refreshControl={
<RefreshControl
progressViewOffset={headerHeight}
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/modules/subscription/SubscriptionLists.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FeedViewType } from "@follow/constants"
import type { FlashList } from "@shopify/flash-list"
import { router } from "expo-router"
import { useMemo, useState } from "react"
import { Text } from "react-native"
Expand Down Expand Up @@ -46,7 +47,7 @@ export const SubscriptionList = ({ view }: { view: FeedViewType }) => {
return subscriptionSyncService.fetch(view)
})

const scrollViewRef = useRegisterNavigationScrollView()
const scrollViewRef = useRegisterNavigationScrollView<FlashList<any>>()

return (
<TimelineSelectorList
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/screens/(stack)/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getDefaultHeaderHeight } from "@react-navigation/elements"
import { useIsFocused } from "@react-navigation/native"
import { createNativeStackNavigator } from "@react-navigation/native-stack"
import { createContext, useCallback, useContext, useEffect, useState } from "react"
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
import type { NativeScrollEvent, NativeSyntheticEvent, ScrollView } from "react-native"
import { findNodeHandle, Text, UIManager } from "react-native"
import type { SharedValue } from "react-native-reanimated"
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"
Expand Down Expand Up @@ -69,7 +69,7 @@ function Settings() {
[opacity],
)
const [contentSize, setContentSize] = useState({ height: 0, width: 0 })
const registerNavigationScrollView = useRegisterNavigationScrollView()
const registerNavigationScrollView = useRegisterNavigationScrollView<ScrollView>()
useEffect(() => {
if (!isFocused) return
const scrollView = registerNavigationScrollView.current
Expand Down

0 comments on commit 34540a1

Please sign in to comment.