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:
parent
fce03fe977
commit
150fbf7bac
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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} />,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ export default function ModalLayout() {
|
|||
title: "List",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: "Profile",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
Loading…
Reference in New Issue