refactor(mobile): remove deprecated profile screen and update navigation

- Remove ProfileScreen from settings routes
- Comment out profile-related navigation links
- Add profile route to modal layout
- Update HomeLeftAction to use expo-router for profile navigation

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-02-24 23:23:46 +08:00
parent fce03fe977
commit 150fbf7bac
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
6 changed files with 285 additions and 15 deletions

View File

@ -1,4 +1,5 @@
import { cn } from "@follow/utils"
import { router } from "expo-router"
import type { PropsWithChildren } from "react"
import { TouchableOpacity, View } from "react-native"
@ -27,7 +28,7 @@ export function HomeLeftAction() {
if (!user) return null
return (
<ActionGroup>
<TouchableOpacity>
<TouchableOpacity onPress={() => router.push("/profile")}>
<UserAvatar image={user.image} name={user.name!} size={28} />
</TouchableOpacity>
</ActionGroup>

View File

@ -25,7 +25,6 @@ import { SafeLockFilledIcon } from "@/src/icons/safe_lock_filled"
import { Settings1CuteFiIcon } from "@/src/icons/settings_1_cute_fi"
import { StarCuteFiIcon } from "@/src/icons/star_cute_fi"
import { TrophyCuteFiIcon } from "@/src/icons/trophy_cute_fi"
import { User3CuteFiIcon } from "@/src/icons/user_3_cute_fi"
import { UserSettingCuteFiIcon } from "@/src/icons/user_setting_cute_fi"
import { signOut } from "@/src/lib/auth"
@ -43,17 +42,17 @@ interface GroupNavigationLink {
todo?: boolean
}
const UserGroupNavigationLinks: GroupNavigationLink[] = [
{
label: "Profile",
icon: User3CuteFiIcon,
onPress: (navigation, scrollRef) => {
scrollRef.current?.scrollTo({ y: 0, animated: true })
setTimeout(() => {
navigation.navigate("Profile")
}, 100)
},
iconBackgroundColor: "#4F46E5",
},
// {
// label: "Profile",
// icon: User3CuteFiIcon,
// onPress: (navigation, scrollRef) => {
// scrollRef.current?.scrollTo({ y: 0, animated: true })
// setTimeout(() => {
// navigation.navigate("Profile")
// }, 100)
// },
// iconBackgroundColor: "#4F46E5",
// },
{
label: "Achievement",
icon: TrophyCuteFiIcon,

View File

@ -35,6 +35,9 @@ import { useShareSubscription } from "../hooks/useShareSubscription"
import { UserHeaderBanner } from "../UserHeaderBanner"
type Subscription = Awaited<ReturnType<typeof apiClient.subscriptions.$get>>["data"][number]
/**
* @deprecated
*/
export const ProfileScreen = () => {
const scrollY = useSharedValue(0)
const { data: subscriptions, isLoading, isError } = useShareSubscription()

View File

@ -12,11 +12,10 @@ import { ListsScreen } from "./Lists"
import { ManageListScreen } from "./ManageList"
import { NotificationsScreen } from "./Notifications"
import { PrivacyScreen } from "./Privacy"
import { ProfileScreen } from "./Profile"
export const SettingRoutes = (Stack: TypedNavigator<any, any>) => {
return [
<Stack.Screen key="Profile" name="Profile" component={ProfileScreen} />,
// <Stack.Screen key="Profile" name="Profile" component={ProfileScreen} />,
<Stack.Screen key="Achievement" name="Achievement" component={AchievementScreen} />,
<Stack.Screen key="General" name="General" component={GeneralScreen} />,
<Stack.Screen key="Notifications" name="Notifications" component={NotificationsScreen} />,

View File

@ -27,6 +27,13 @@ export default function ModalLayout() {
title: "List",
}}
/>
<Stack.Screen
name="profile"
options={{
title: "Profile",
headerShown: false,
}}
/>
</Stack>
)
}

View File

@ -0,0 +1,261 @@
import type { FeedViewType } from "@follow/constants"
import { cn } from "@follow/utils"
import { getDefaultHeaderHeight } from "@react-navigation/elements"
import { Fragment, useCallback, useEffect, useMemo } from "react"
import {
ActivityIndicator,
FlatList,
Image,
Share,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native"
import Animated, { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated"
import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context"
import { ReAnimatedScrollView } from "@/src/components/common/AnimatedComponents"
import { BlurEffect } from "@/src/components/common/BlurEffect"
import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon"
import type { FeedIconRequiredFeed } from "@/src/components/ui/icon/feed-icon"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { Share3CuteReIcon } from "@/src/icons/share_3_cute_re"
import type { apiClient } from "@/src/lib/api-fetch"
import { toast } from "@/src/lib/toast"
import { useShareSubscription } from "@/src/modules/settings/hooks/useShareSubscription"
import { UserHeaderBanner } from "@/src/modules/settings/UserHeaderBanner"
import { ItemSeparator } from "@/src/modules/subscription/ItemSeparator"
import type { FeedModel } from "@/src/store/feed/types"
import type { ListModel } from "@/src/store/list/store"
import { useWhoami } from "@/src/store/user/hooks"
import { useColor } from "@/src/theme/colors"
type Subscription = Awaited<ReturnType<typeof apiClient.subscriptions.$get>>["data"][number]
export default function ProfileScreen() {
const scrollY = useSharedValue(0)
const { data: subscriptions, isLoading, isError } = useShareSubscription()
const headerOpacity = useSharedValue(0)
const scrollHandler = useAnimatedScrollHandler((event) => {
scrollY.value = event.contentOffset.y
headerOpacity.value = scrollY.value / 100
})
const whoami = useWhoami()
useEffect(() => {
if (isError) {
toast.error("Failed to fetch subscriptions")
}
}, [isError])
const insets = useSafeAreaInsets()
const textLabelColor = useColor("label")
const openShareUrl = useCallback(() => {
if (!whoami?.id) return
Share.share({
url: `https://app.follow.is/share/users/${whoami.id}`,
title: `Follow | ${whoami.name}'s Profile`,
})
}, [whoami?.id, whoami?.name])
const frame = useSafeAreaFrame()
const headerHeight = getDefaultHeaderHeight(frame, false, 0)
return (
<View className="bg-system-grouped-background flex-1">
<ReAnimatedScrollView
nestedScrollEnabled
onScroll={scrollHandler}
contentContainerStyle={{ paddingBottom: insets.bottom, paddingTop: headerHeight }}
>
<UserHeaderBanner scrollY={scrollY} />
{isLoading && <ActivityIndicator className="mt-24" size={28} />}
{!isLoading && subscriptions && <SubscriptionList subscriptions={subscriptions.data} />}
</ReAnimatedScrollView>
<View
style={{ height: headerHeight }}
className="absolute top-0 w-full flex-row items-center justify-between px-4"
>
<View />
<TouchableOpacity onPress={openShareUrl}>
<Share3CuteReIcon color="#fff" />
</TouchableOpacity>
</View>
{/* Header */}
<Animated.View
pointerEvents="none"
className="border-b-hairline border-opaque-separator absolute inset-x-0 top-0 flex-row items-center px-4"
style={{ opacity: headerOpacity, height: headerHeight }}
>
<BlurEffect />
<Text className="text-label flex-1 text-center text-lg font-medium">
{whoami?.name}'s Profile
</Text>
<TouchableOpacity onPress={openShareUrl}>
<Share3CuteReIcon color={textLabelColor} />
</TouchableOpacity>
</Animated.View>
</View>
)
}
type PickedListModel = Pick<ListModel, "id" | "title" | "image" | "description" | "view"> & {
customTitle?: string | null
}
type PickedFeedModel = Pick<
FeedModel,
"id" | "title" | "description" | "siteUrl" | "url" | "image"
> & {
customTitle?: string | null
view: FeedViewType
}
const SubscriptionList = ({ subscriptions }: { subscriptions: Subscription[] }) => {
const { lists, feeds, groupedFeeds } = useMemo(() => {
const lists = [] as PickedListModel[]
const feeds = [] as PickedFeedModel[]
const groupedFeeds = {} as Record<string, PickedFeedModel[]>
for (const subscription of subscriptions) {
if ("listId" in subscription) {
lists.push({
id: subscription.listId,
title: subscription.lists.title!,
image: subscription.lists.image!,
description: subscription.lists.description!,
view: subscription.lists.view,
customTitle: subscription.title,
})
continue
}
if ("feedId" in subscription && "feeds" in subscription) {
const feed = {
id: subscription.feedId,
title: subscription.feeds.title!,
image: subscription.feeds.image!,
description: subscription.feeds.description!,
siteUrl: subscription.feeds.siteUrl!,
url: subscription.feeds.url!,
view: subscription.view as FeedViewType,
customTitle: subscription.title,
}
if (subscription.category) {
groupedFeeds[subscription.category] = [
...(groupedFeeds[subscription.category] || []),
feed,
]
} else {
feeds.push(feed)
}
}
}
return { lists, feeds, groupedFeeds }
}, [subscriptions])
const hasFeeds = Object.keys(groupedFeeds).length > 0 || feeds.length > 0
return (
<View>
{lists.length > 0 && (
<Fragment>
<SectionHeader title="Lists" />
<FlatList
scrollEnabled={false}
data={lists}
renderItem={renderListItems}
ItemSeparatorComponent={ItemSeparator}
/>
</Fragment>
)}
{hasFeeds && (
<View className="mt-4">
<SectionHeader title="Feeds" />
{Object.entries(groupedFeeds).map(([category, feeds], index) => (
<Fragment key={category}>
<Text
className={cn(
"text-secondary-label mb-2 ml-3 text-sm font-medium",
index !== 0 ? "mt-6" : "",
)}
>
{category}
</Text>
<FlatList
scrollEnabled={false}
data={feeds}
renderItem={renderFeedItems}
ItemSeparatorComponent={ItemSeparator}
/>
</Fragment>
))}
<Text className="text-secondary-label mb-2 ml-3 mt-6 text-sm font-medium">
Uncategorized Feeds
</Text>
<FlatList
scrollEnabled={false}
data={feeds}
renderItem={renderFeedItems}
ItemSeparatorComponent={ItemSeparator}
/>
</View>
)}
</View>
)
}
const renderListItems = ({ item }: { item: PickedListModel }) => (
<View className="bg-secondary-system-grouped-background h-12 flex-row items-center px-3">
<View className="overflow-hidden rounded">
{!!item.image && (
<Image source={{ uri: item.image, width: 24, height: 24 }} resizeMode="cover" />
)}
{!item.image && <FallbackIcon title={item.title} size={24} />}
</View>
<Text className="text-text ml-2">{item.title}</Text>
</View>
)
const renderFeedItems = ({ item }: { item: PickedFeedModel }) => (
<View className="bg-secondary-system-grouped-background h-12 flex-row items-center px-3">
<View className="overflow-hidden rounded">
<FeedIcon
feed={
{
id: item.id,
title: item.title,
url: item.url,
image: item.image,
type: item.view,
siteUrl: item.siteUrl || "",
} as FeedIconRequiredFeed
}
size={24}
/>
</View>
<Text className="text-text ml-2">{item.title}</Text>
</View>
)
const SectionHeader = ({ title }: { title: string }) => (
<View className="my-5 flex-row items-center justify-center gap-4">
<View
className="bg-secondary-label w-12 rounded-full"
style={{ height: StyleSheet.hairlineWidth }}
/>
<Text className="text-secondary-label text-sm font-medium">{title}</Text>
<View
className="bg-secondary-label w-12 rounded-full"
style={{ height: StyleSheet.hairlineWidth }}
/>
</View>
)