diff --git a/apps/mobile/src/modules/screen/action.tsx b/apps/mobile/src/modules/screen/action.tsx index 8c37b9306..495b31727 100644 --- a/apps/mobile/src/modules/screen/action.tsx +++ b/apps/mobile/src/modules/screen/action.tsx @@ -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 ( - + router.push("/profile")}> diff --git a/apps/mobile/src/modules/settings/SettingsList.tsx b/apps/mobile/src/modules/settings/SettingsList.tsx index 05aa0d107..929c9d339 100644 --- a/apps/mobile/src/modules/settings/SettingsList.tsx +++ b/apps/mobile/src/modules/settings/SettingsList.tsx @@ -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, diff --git a/apps/mobile/src/modules/settings/routes/Profile.tsx b/apps/mobile/src/modules/settings/routes/Profile.tsx index 9bc8761cb..1de43741e 100644 --- a/apps/mobile/src/modules/settings/routes/Profile.tsx +++ b/apps/mobile/src/modules/settings/routes/Profile.tsx @@ -35,6 +35,9 @@ import { useShareSubscription } from "../hooks/useShareSubscription" import { UserHeaderBanner } from "../UserHeaderBanner" type Subscription = Awaited>["data"][number] +/** + * @deprecated + */ export const ProfileScreen = () => { const scrollY = useSharedValue(0) const { data: subscriptions, isLoading, isError } = useShareSubscription() diff --git a/apps/mobile/src/modules/settings/routes/index.tsx b/apps/mobile/src/modules/settings/routes/index.tsx index 57fca3c19..812d8d60c 100644 --- a/apps/mobile/src/modules/settings/routes/index.tsx +++ b/apps/mobile/src/modules/settings/routes/index.tsx @@ -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) => { return [ - , + // , , , , diff --git a/apps/mobile/src/screens/(modal)/_layout.tsx b/apps/mobile/src/screens/(modal)/_layout.tsx index aaf78fa4d..6c0d24337 100644 --- a/apps/mobile/src/screens/(modal)/_layout.tsx +++ b/apps/mobile/src/screens/(modal)/_layout.tsx @@ -27,6 +27,13 @@ export default function ModalLayout() { title: "List", }} /> + ) } diff --git a/apps/mobile/src/screens/(modal)/profile.tsx b/apps/mobile/src/screens/(modal)/profile.tsx new file mode 100644 index 000000000..b57d77ccf --- /dev/null +++ b/apps/mobile/src/screens/(modal)/profile.tsx @@ -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>["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 ( + + + + + {isLoading && } + {!isLoading && subscriptions && } + + + + + + + + + {/* Header */} + + + + + {whoami?.name}'s Profile + + + + + + + + ) +} + +type PickedListModel = Pick & { + 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 + + 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 ( + + {lists.length > 0 && ( + + + + + + )} + {hasFeeds && ( + + + {Object.entries(groupedFeeds).map(([category, feeds], index) => ( + + + {category} + + + + ))} + + + Uncategorized Feeds + + + + )} + + ) +} +const renderListItems = ({ item }: { item: PickedListModel }) => ( + + + {!!item.image && ( + + )} + {!item.image && } + + + {item.title} + +) + +const renderFeedItems = ({ item }: { item: PickedFeedModel }) => ( + + + + + {item.title} + +) + +const SectionHeader = ({ title }: { title: string }) => ( + + + {title} + + +)