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 <tukon479@gmail.com>
This commit is contained in:
parent
6c9448ccd1
commit
f52026a3ce
|
|
@ -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> = T extends UseQueryResult<infer T> ? 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<typeof query>
|
||||
queryClient.cancelQueries({ queryKey })
|
||||
queryClient.setQueryData<QueryData>(queryKey, (data) => {
|
||||
if (!data) return
|
||||
return {
|
||||
...data,
|
||||
data: data?.data.filter((item) => item.feedId !== id),
|
||||
}
|
||||
})
|
||||
},
|
||||
[queryClient, queryKey],
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ReturnType<typeof apiClient.subscriptions.$get>>["data"][number]
|
||||
|
||||
export const ProfileScreen: NavigationControllerView<{
|
||||
|
|
@ -57,6 +65,13 @@ export const ProfileScreen: NavigationControllerView<{
|
|||
return <ProfileScreenImpl userId={userId || whoami?.id} />
|
||||
}
|
||||
|
||||
const IsMyProfileContext = createContext<boolean>(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 (
|
||||
<View className="bg-system-grouped-background flex-1">
|
||||
<Animated.View
|
||||
|
|
@ -113,6 +134,7 @@ function ProfileScreenImpl(props: { userId: string }) {
|
|||
style={{ opacity: headerOpacity }}
|
||||
>
|
||||
<BlurEffect />
|
||||
|
||||
<InternalNavigationHeader
|
||||
title={t("profile.title", {
|
||||
name: user?.name || user?.handle,
|
||||
|
|
@ -132,7 +154,11 @@ function ProfileScreenImpl(props: { userId: string }) {
|
|||
<UserHeaderBanner scrollY={scrollY} userId={props.userId} />
|
||||
|
||||
{isLoading && <PlatformActivityIndicator className="mt-24" size={28} />}
|
||||
{!isLoading && subscriptions && <SubscriptionList subscriptions={subscriptions.data} />}
|
||||
<IsMyProfileContext.Provider value={isMyProfile}>
|
||||
<ActionContext.Provider value={actionCtx}>
|
||||
{!isLoading && subscriptions && <SubscriptionList subscriptions={subscriptions.data} />}
|
||||
</ActionContext.Provider>
|
||||
</IsMyProfileContext.Provider>
|
||||
</ReAnimatedScrollView>
|
||||
|
||||
<Animated.View
|
||||
|
|
@ -259,9 +285,21 @@ const SubscriptionList = ({ subscriptions }: { subscriptions: Subscription[] })
|
|||
)
|
||||
}
|
||||
const renderListItems = ({ item }: { item: PickedListModel }) => (
|
||||
<View
|
||||
<ItemPressable
|
||||
className="bg-secondary-system-grouped-background flex h-12 flex-row items-center"
|
||||
style={{ paddingHorizontal: GROUPED_LIST_ITEM_PADDING }}
|
||||
onPress={() => {
|
||||
if (getSubscription(item.id))
|
||||
Navigation.rootNavigation.pushControllerView(FeedScreen, {
|
||||
feedId: item.id,
|
||||
})
|
||||
else {
|
||||
Navigation.rootNavigation.pushControllerView(FollowScreen, {
|
||||
type: "list",
|
||||
id: item.id,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View className="overflow-hidden rounded">
|
||||
{!!item.image && (
|
||||
|
|
@ -277,39 +315,91 @@ const renderListItems = ({ item }: { item: PickedListModel }) => (
|
|||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
</View>
|
||||
</ItemPressable>
|
||||
)
|
||||
|
||||
const renderFeedItems = ({ item }: { item: PickedFeedModel }) => (
|
||||
<View
|
||||
className="bg-secondary-system-grouped-background flex h-12 flex-row items-center"
|
||||
style={{ paddingHorizontal: GROUPED_LIST_ITEM_PADDING }}
|
||||
>
|
||||
<View className="overflow-hidden rounded">
|
||||
<FeedIcon
|
||||
feed={
|
||||
{
|
||||
<MaybeSwipeable id={item.id}>
|
||||
<ItemPressable
|
||||
className="bg-secondary-system-grouped-background flex h-12 flex-row items-center"
|
||||
style={{ paddingHorizontal: GROUPED_LIST_ITEM_PADDING }}
|
||||
onPress={() => {
|
||||
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}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
className="text-text mr-4"
|
||||
numberOfLines={1}
|
||||
style={{ marginLeft: GROUPED_ICON_TEXT_GAP }}
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
</View>
|
||||
<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 mr-4"
|
||||
numberOfLines={1}
|
||||
style={{ marginLeft: GROUPED_ICON_TEXT_GAP }}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
</ItemPressable>
|
||||
</MaybeSwipeable>
|
||||
)
|
||||
|
||||
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 (
|
||||
<SwipeableItem
|
||||
rightActions={[
|
||||
{
|
||||
onPress: () => {
|
||||
// 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}
|
||||
</SwipeableItem>
|
||||
)
|
||||
}
|
||||
|
||||
const SectionHeader = ({ title }: { title: string }) => (
|
||||
<View className="mb-2 mt-5" style={{ marginHorizontal: GROUPED_LIST_MARGIN }}>
|
||||
<Text
|
||||
|
|
|
|||
Loading…
Reference in New Issue