From f52026a3ce85eddb29525e20aac25b361d2ec01d Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 16 Apr 2025 22:46:49 +0800 Subject: [PATCH] feat(mobile): enhance share subscription functionality in ProfileScreen - Updated useShareSubscription hook to include a removeItemById method for managing subscriptions. - Integrated the new functionality into ProfileScreen, allowing users to unsubscribe from feeds. - Added context providers for managing profile state and actions. - Refactored list item components to utilize ItemPressable for better interaction handling. Signed-off-by: Innei --- .../settings/hooks/useShareSubscription.tsx | 29 +++- .../src/screens/(modal)/ProfileScreen.tsx | 148 ++++++++++++++---- 2 files changed, 145 insertions(+), 32 deletions(-) diff --git a/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx b/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx index 9ebf43764..acd7c69a3 100644 --- a/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx +++ b/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx @@ -1,10 +1,15 @@ -import { useQuery } from "@tanstack/react-query" +import type { UseQueryResult } from "@tanstack/react-query" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { useCallback, useMemo } from "react" import { apiClient } from "@/src/lib/api-fetch" +// eslint-disable-next-line unused-imports/no-unused-vars +type ExtractQueryData = T extends UseQueryResult ? T : never export const useShareSubscription = ({ userId }: { userId: string }) => { - return useQuery({ - queryKey: ["public", "subscription", userId], + const queryKey = useMemo(() => ["public", "subscription", userId], [userId]) + const query = useQuery({ + queryKey, queryFn: async () => { const subscriptions = await apiClient.subscriptions.$get({ query: { @@ -15,4 +20,22 @@ export const useShareSubscription = ({ userId }: { userId: string }) => { return subscriptions }, }) + const queryClient = useQueryClient() + return { + ...query, + removeItemById: useCallback( + (id: string) => { + type QueryData = ExtractQueryData + queryClient.cancelQueries({ queryKey }) + queryClient.setQueryData(queryKey, (data) => { + if (!data) return + return { + ...data, + data: data?.data.filter((item) => item.feedId !== id), + } + }) + }, + [queryClient, queryKey], + ), + } } diff --git a/apps/mobile/src/screens/(modal)/ProfileScreen.tsx b/apps/mobile/src/screens/(modal)/ProfileScreen.tsx index 571424d6d..95ba64545 100644 --- a/apps/mobile/src/screens/(modal)/ProfileScreen.tsx +++ b/apps/mobile/src/screens/(modal)/ProfileScreen.tsx @@ -1,8 +1,8 @@ import type { FeedViewType } from "@follow/constants" import { useQuery } from "@tanstack/react-query" -import { Fragment, useCallback, useEffect, useMemo } from "react" +import { createContext, Fragment, useCallback, useContext, useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" -import { FlatList, Image, Share, Text, TouchableOpacity, View } from "react-native" +import { Alert, FlatList, Image, Share, Text, TouchableOpacity, View } from "react-native" import Animated, { interpolate, useAnimatedScrollHandler, @@ -13,6 +13,7 @@ import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-cont import { ReAnimatedScrollView } from "@/src/components/common/AnimatedComponents" import { BlurEffect } from "@/src/components/common/BlurEffect" +import { SwipeableItem } from "@/src/components/common/SwipeableItem" import { InternalNavigationHeader, UINavigationHeaderActionButton, @@ -31,8 +32,10 @@ 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 { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator" +import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable" import { ShareForwardCuteReIcon } from "@/src/icons/share_forward_cute_re" import type { apiClient } from "@/src/lib/api-fetch" +import { Navigation } from "@/src/lib/navigation/Navigation" import type { NavigationControllerView } from "@/src/lib/navigation/types" import { toast } from "@/src/lib/toast" import { useShareSubscription } from "@/src/modules/settings/hooks/useShareSubscription" @@ -40,10 +43,15 @@ 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 { getSubscription } from "@/src/store/subscription/getter" +import { subscriptionSyncService } from "@/src/store/subscription/store" import { useUser, useWhoami } from "@/src/store/user/hooks" import { userSyncService } from "@/src/store/user/store" import { useColor } from "@/src/theme/colors" +import { FeedScreen } from "../(stack)/feeds/[feedId]/FeedScreen" +import { FollowScreen } from "./FollowScreen" + type Subscription = Awaited>["data"][number] export const ProfileScreen: NavigationControllerView<{ @@ -57,6 +65,13 @@ export const ProfileScreen: NavigationControllerView<{ return } +const IsMyProfileContext = createContext(false) + +const ActionContext = createContext<{ + removeItemById: (id: string) => void +}>({ + removeItemById: () => {}, +}) const usePrefetchUser = (userId: string) => { const { data } = useQuery({ queryKey: ["user", userId], @@ -73,6 +88,8 @@ function ProfileScreenImpl(props: { userId: string }) { data: subscriptions, isLoading, isError, + + removeItemById, } = useShareSubscription({ userId: props.userId, }) @@ -105,6 +122,10 @@ function ProfileScreenImpl(props: { userId: string }) { const frame = useSafeAreaFrame() const headerHeight = getDefaultHeaderHeight(frame, false, 0) + const whoami = useWhoami() + const isMyProfile = user?.id === whoami?.id + const actionCtx = useMemo(() => ({ removeItemById }), [removeItemById]) + return ( + {isLoading && } - {!isLoading && subscriptions && } + + + {!isLoading && subscriptions && } + + ( - { + if (getSubscription(item.id)) + Navigation.rootNavigation.pushControllerView(FeedScreen, { + feedId: item.id, + }) + else { + Navigation.rootNavigation.pushControllerView(FollowScreen, { + type: "list", + id: item.id, + }) + } + }} > {!!item.image && ( @@ -277,39 +315,91 @@ const renderListItems = ({ item }: { item: PickedListModel }) => ( > {item.title} - + ) const renderFeedItems = ({ item }: { item: PickedFeedModel }) => ( - - - + { + if (getSubscription(item.id)) + Navigation.rootNavigation.pushControllerView(FeedScreen, { + feedId: item.id, + }) + else { + Navigation.rootNavigation.pushControllerView(FollowScreen, { + type: "feed", id: item.id, - title: item.title, - url: item.url, - image: item.image, - type: item.view, - siteUrl: item.siteUrl || "", - } as FeedIconRequiredFeed + }) } - size={24} - /> - - - {item.title} - - + + + + + {item.title} + + + ) +const MaybeSwipeable = ({ id, children }: { id: string; children: React.ReactNode }) => { + const isMyProfile = useContext(IsMyProfileContext) + const { t } = useTranslation() + const { removeItemById } = useContext(ActionContext) + if (!isMyProfile) { + return children + } + return ( + { + // unsubscribe + Alert.alert("Unsubscribe?", "This will remove the feed from your subscriptions", [ + { + text: "Cancel", + style: "cancel", + }, + { + text: t("operation.unfollow"), + style: "destructive", + onPress: () => { + subscriptionSyncService.unsubscribe(id) + removeItemById(id) + }, + }, + ]) + }, + backgroundColor: "red", + label: t("operation.unfollow"), + }, + ]} + > + {children} + + ) +} + const SectionHeader = ({ title }: { title: string }) => (