refactor(mobile): Replace TimelineSelectorHeader with TimelineSelectorProvider

- Remove TimelineSelectorHeader component
- Introduce TimelineSelectorProvider to manage timeline context
- Update entry list, subscriptions, and feed screens to use new provider
- Simplify component structure and improve context management

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-02-19 20:29:42 +08:00
parent bdff613867
commit ba0c9119dc
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
7 changed files with 152 additions and 159 deletions

View File

@ -1,7 +1,7 @@
import { FeedViewType } from "@follow/constants"
import { useIsFocused } from "@react-navigation/native"
import { useEffect, useMemo } from "react"
import { Animated, StyleSheet, View } from "react-native"
import { Animated, StyleSheet } from "react-native"
import PagerView from "react-native-pager-view"
import { views } from "@/src/constants/views"
@ -19,7 +19,7 @@ import {
} from "@/src/store/entry/hooks"
import { useListEntryIds } from "@/src/store/list/hooks"
import { TimelineSelectorHeader } from "../screen/TimelineSelectorHeader"
import { TimelineSelectorProvider } from "../screen/TimelineSelectorProvider"
import { EntryListSelector } from "./EntryListSelector"
import { usePagerView } from "./usePagerView"
@ -35,26 +35,30 @@ export function EntryList() {
}, [setDrawerSwipeDisabled, isFocused])
const selectedFeed = useSelectedFeed()
if (!selectedFeed) return null
switch (selectedFeed.type) {
case "view": {
return <ViewPagerList viewId={selectedFeed.viewId} />
const Content = useMemo(() => {
if (!selectedFeed) return null
switch (selectedFeed.type) {
case "view": {
return <ViewPagerList viewId={selectedFeed.viewId} />
}
case "feed": {
return <FeedEntryList feedId={selectedFeed.feedId} />
}
case "category": {
return <CategoryEntryList categoryName={selectedFeed.categoryName} />
}
case "list": {
return <ListEntryList listId={selectedFeed.listId} />
}
case "inbox": {
return <InboxEntryList inboxId={selectedFeed.inboxId} />
}
}
case "feed": {
return <FeedEntryList feedId={selectedFeed.feedId} />
}
case "category": {
return <CategoryEntryList categoryName={selectedFeed.categoryName} />
}
case "list": {
return <ListEntryList listId={selectedFeed.listId} />
}
case "inbox": {
return <InboxEntryList inboxId={selectedFeed.inboxId} />
}
// No default
}
}, [selectedFeed])
if (!Content) return null
return <TimelineSelectorProvider>{Content}</TimelineSelectorProvider>
}
const AnimatedPagerView = Animated.createAnimatedComponent<typeof PagerView>(PagerView)
@ -73,33 +77,21 @@ function ViewPagerList({ viewId }: { viewId: FeedViewType }) {
}, [page, pagerRef, viewId])
return (
<View style={styles.container}>
<TimelineSelectorHeader>
<AnimatedPagerView
testID="pager-view"
ref={pagerRef}
style={styles.PagerView}
initialPage={page}
layoutDirection="ltr"
overdrag
onPageScroll={rest.onPageScroll}
onPageSelected={rest.onPageSelected}
onPageScrollStateChanged={rest.onPageScrollStateChanged}
pageMargin={10}
orientation="horizontal"
>
{useMemo(
() =>
views.map((view) => (
<View testID="pager-view-content" key={view.view}>
<ViewEntryList viewId={view.view} />
</View>
)),
[],
)}
</AnimatedPagerView>
</TimelineSelectorHeader>
</View>
<AnimatedPagerView
testID="pager-view"
ref={pagerRef}
style={styles.PagerView}
initialPage={page}
layoutDirection="ltr"
overdrag
onPageScroll={rest.onPageScroll}
onPageSelected={rest.onPageSelected}
onPageScrollStateChanged={rest.onPageScrollStateChanged}
pageMargin={10}
orientation="horizontal"
>
{useMemo(() => views.map((view) => <ViewEntryList viewId={view.view} key={view.view} />), [])}
</AnimatedPagerView>
)
}

View File

@ -74,6 +74,7 @@ export function EntryGridItem(props: MasonryItem) {
}
case "video": {
const { videoPreviewImageUrl } = props
return (
<>
{videoPreviewImageUrl ? (

View File

@ -27,13 +27,15 @@ export const TimelineSelectorList = forwardRef<
const insets = useSafeAreaInsets()
const headerHeight = useHeaderHeight()
const scrollY = useContext(NavigationContext)?.scrollY
const { scrollY } = useContext(NavigationContext)!
const onScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
props.onScroll?.(e)
scrollY?.setValue(e.nativeEvent.contentOffset.y)
},
[scrollY],
[scrollY, props.onScroll],
)
const tabBarHeight = useBottomTabBarHeight()
@ -56,7 +58,6 @@ export const TimelineSelectorList = forwardRef<
refreshing={isRefetching}
/>
}
onScroll={onScroll}
scrollIndicatorInsets={{
top: headerHeight - insets.top,
bottom: tabBarHeight ? tabBarHeight - insets.bottom : undefined,
@ -66,6 +67,7 @@ export const TimelineSelectorList = forwardRef<
paddingBottom: tabBarHeight,
}}
{...props}
onScroll={onScroll}
/>
)
})
@ -85,9 +87,10 @@ export const TimelineSelectorMasonryList = ({
const onScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
props.onScroll?.(e)
scrollY?.setValue(e.nativeEvent.contentOffset.y)
},
[scrollY],
[scrollY, props.onScroll],
)
const tabBarHeight = useBottomTabBarHeight()
@ -109,7 +112,6 @@ export const TimelineSelectorMasonryList = ({
refreshing={isRefetching}
/>
}
onScroll={onScroll}
scrollIndicatorInsets={{
top: headerHeight - insets.top,
bottom: tabBarHeight ? tabBarHeight - insets.bottom : undefined,
@ -119,6 +121,7 @@ export const TimelineSelectorMasonryList = ({
paddingBottom: tabBarHeight,
}}
{...props}
onScroll={onScroll}
/>
)
}

View File

@ -22,7 +22,7 @@ import { getFeed } from "@/src/store/feed/getter"
import { useEntryListContext, useSelectedFeedTitle, useSelectedView } from "./atoms"
export function TimelineSelectorHeader({ children }: { children: React.ReactNode }) {
export function TimelineSelectorProvider({ children }: { children: React.ReactNode }) {
const scrollY = useAnimatedValue(0)
const viewTitle = useSelectedFeedTitle()
const screenType = useEntryListContext().type

View File

@ -22,7 +22,6 @@ import { Search3CuteFiIcon } from "@/src/icons/search_3_cute_fi"
import { Search3CuteReIcon } from "@/src/icons/search_3_cute_re"
import { Settings1CuteFiIcon } from "@/src/icons/settings_1_cute_fi"
import { Settings1CuteReIcon } from "@/src/icons/settings_1_cute_re"
import { FeedDrawer } from "@/src/modules/feed-drawer/drawer"
import { useColor } from "@/src/theme/colors"
const fifthTap = Gesture.Tap()
@ -46,112 +45,110 @@ export default function TabLayout() {
}, [animatedTransformY, tabBarVisible])
return (
<FeedDrawer>
<BottomTabBarBackgroundContext.Provider value={useMemo(() => ({ opacity }), [opacity])}>
<SetBottomTabBarVisibleContext.Provider value={setTabBarVisible}>
<Tabs
screenListeners={{
tabPress: () => {
opacity.value = 1
<BottomTabBarBackgroundContext.Provider value={useMemo(() => ({ opacity }), [opacity])}>
<SetBottomTabBarVisibleContext.Provider value={setTabBarVisible}>
<Tabs
screenListeners={{
tabPress: () => {
opacity.value = 1
},
transitionStart: () => {
opacity.value = 1
},
}}
screenOptions={{
tabBarBackground: TabBarBackground,
tabBarStyle: {
position: "absolute",
borderTopWidth: 0,
transform: [
{
translateY: animatedTransformY.interpolate({
inputRange: [0, 1],
outputRange: [100, 0],
}),
},
],
},
animation: "fade",
transitionSpec: {
animation: "timing",
config: {
duration: 50,
easing: Easing.ease,
},
transitionStart: () => {
opacity.value = 1
},
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
headerShown: false,
tabBarLabel: "Timeline",
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? Home5CuteReIcon : Home5CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
tabBarButton(props) {
return <Pressable {...props} />
},
}}
screenOptions={{
tabBarBackground: TabBarBackground,
tabBarStyle: {
position: "absolute",
borderTopWidth: 0,
transform: [
{
translateY: animatedTransformY.interpolate({
inputRange: [0, 1],
outputRange: [100, 0],
}),
},
],
/>
<Tabs.Screen
name="subscriptions"
options={{
title: "Subscriptions",
headerShown: false,
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? BlackBoard2CuteReIcon : BlackBoard2CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
animation: "fade",
transitionSpec: {
animation: "timing",
config: {
duration: 50,
easing: Easing.ease,
},
tabBarLabel: "Subscriptions",
tabBarButton(props) {
return <Pressable {...props} />
},
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
headerShown: false,
tabBarLabel: "Timeline",
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? Home5CuteReIcon : Home5CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
tabBarButton(props) {
return <Pressable {...props} />
},
}}
/>
<Tabs.Screen
name="subscriptions"
options={{
title: "Subscriptions",
headerShown: false,
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? BlackBoard2CuteReIcon : BlackBoard2CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
tabBarLabel: "Subscriptions",
/>
<Tabs.Screen
name="discover"
options={{
title: "Discover",
headerShown: false,
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? Search3CuteReIcon : Search3CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
tabBarButton(props) {
return <Pressable {...props} />
},
}}
/>
tabBarButton(props) {
return <Pressable {...props} />
},
}}
/>
<Tabs.Screen
name="discover"
options={{
title: "Discover",
headerShown: false,
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? Search3CuteReIcon : Search3CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
tabBarButton(props) {
return <Pressable {...props} />
},
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
headerShown: false,
tabBarButton(props) {
return (
<GestureDetector gesture={fifthTap}>
<View className="flex-1">
<Pressable {...props} />
</View>
</GestureDetector>
)
},
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? Settings1CuteReIcon : Settings1CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
}}
/>
</Tabs>
</SetBottomTabBarVisibleContext.Provider>
</BottomTabBarBackgroundContext.Provider>
</FeedDrawer>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
headerShown: false,
tabBarButton(props) {
return (
<GestureDetector gesture={fifthTap}>
<View className="flex-1">
<Pressable {...props} />
</View>
</GestureDetector>
)
},
tabBarIcon: ({ color, focused }) => {
const Icon = !focused ? Settings1CuteReIcon : Settings1CuteFiIcon
return <Icon color={color} width={24} height={24} />
},
}}
/>
</Tabs>
</SetBottomTabBarVisibleContext.Provider>
</BottomTabBarBackgroundContext.Provider>
)
}

View File

@ -1,5 +1,5 @@
import { useSelectedFeed } from "@/src/modules/screen/atoms"
import { TimelineSelectorHeader } from "@/src/modules/screen/TimelineSelectorHeader"
import { TimelineSelectorProvider } from "@/src/modules/screen/TimelineSelectorProvider"
import { SubscriptionList } from "@/src/modules/subscription/SubscriptionLists"
export default function Subscriptions() {
@ -7,8 +7,8 @@ export default function Subscriptions() {
const view = selectedFeed?.type === "view" ? selectedFeed.viewId : undefined
return (
<TimelineSelectorHeader>
<TimelineSelectorProvider>
<SubscriptionList view={view ?? 0} />
</TimelineSelectorHeader>
</TimelineSelectorProvider>
)
}

View File

@ -6,7 +6,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"
import { EntryListSelector } from "@/src/modules/entry-list/EntryListSelector"
import { EntryListContext, useSelectedView } from "@/src/modules/screen/atoms"
import { TimelineSelectorHeader } from "@/src/modules/screen/TimelineSelectorHeader"
import { TimelineSelectorProvider } from "@/src/modules/screen/TimelineSelectorProvider"
import { useCollectionEntryList } from "@/src/store/collection/hooks"
import { useEntryIdsByCategory, useEntryIdsByFeedId } from "@/src/store/entry/hooks"
import { FEED_COLLECTION_LIST } from "@/src/store/entry/utils"
@ -29,9 +29,9 @@ export default function Feed() {
return (
<EntryListContext.Provider value={useMemo(() => ({ type: "feed" }), [])}>
<BottomTabBarHeightContext.Provider value={insets.bottom}>
<TimelineSelectorHeader>
<TimelineSelectorProvider>
<EntryListSelector entryIds={entryIds} viewId={view} />
</TimelineSelectorHeader>
</TimelineSelectorProvider>
</BottomTabBarHeightContext.Provider>
</EntryListContext.Provider>
)