Skip to content

Commit

Permalink
feat(rn-list): support list delete
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jan 23, 2025
1 parent bc88121 commit 44c6fd0
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 54 deletions.
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@better-auth/expo": "1.1.9-beta.1",
"@expo/react-native-action-sheet": "4.1.0",
"@expo/vector-icons": "^14.0.2",
"@follow/components": "workspace:*",
"@follow/constants": "workspace:*",
Expand Down
23 changes: 23 additions & 0 deletions apps/mobile/src/components/ui/grouped/GroupedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,29 @@ export const GroupedInsetListActionCell: FC<{
)
}

export const GroupedInsetButtonCell: FC<{
label: string
onPress: () => void
disabled?: boolean
style?: "destructive" | "primary"
}> = ({ label, onPress, disabled, style = "primary" }) => {
return (
<Pressable onPress={onPress} disabled={disabled}>
{({ pressed }) => (
<GroupedInsetListBaseCell
className={cn(pressed ? "bg-system-fill" : undefined, disabled && "opacity-40")}
>
<View className="flex-1 items-center justify-center">
<Text className={`${style === "destructive" ? "text-red" : "text-label"}`}>
{label}
</Text>
</View>
</GroupedInsetListBaseCell>
)}
</Pressable>
)
}

export const GroupedInformationCell: FC<{
title: string
description?: string
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/lib/api-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export const getBizFetchErrorMessage = (error: unknown) => {
return data.message
}
} catch {
return ""
return error.message
}
}
return ""
return error.message
}
91 changes: 55 additions & 36 deletions apps/mobile/src/modules/settings/routes/Lists.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useQuery } from "@tanstack/react-query"
import { router } from "expo-router"
import { createElement, useCallback } from "react"
import { createContext, createElement, useCallback, useContext, useMemo } from "react"
import type { ListRenderItem } from "react-native"
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native"
import Animated, { LinearTransition } from "react-native-reanimated"
Expand All @@ -24,16 +23,16 @@ import { RadaCuteFiIcon } from "@/src/icons/rada_cute_fi"
import { UserAdd2CuteFiIcon } from "@/src/icons/user_add_2_cute_fi"
import { Wallet2CuteFiIcon } from "@/src/icons/wallet_2_cute_fi"
import type { HonoApiClient } from "@/src/morph/types"
import { listSyncServices } from "@/src/store/list/store"
import { useOwnedLists, usePrefetchOwnedLists } from "@/src/store/list/hooks"
import type { ListModel } from "@/src/store/list/store"
import { accentColor } from "@/src/theme/colors"

import { SwipeableGroupProvider, SwipeableItem } from "../../../components/common/SwipeableItem"

const ListContext = createContext({} as Record<string, HonoApiClient.List_List_Get>)
export const ListsScreen = () => {
const { isLoading, data } = useQuery({
queryKey: ["owned", "lists"],
queryFn: () => listSyncServices.fetchOwnedLists(),
})
const { isLoading, data } = usePrefetchOwnedLists()
const lists = useOwnedLists()

return (
<SafeNavigationScrollView nestedScrollEnabled className="bg-system-grouped-background">
Expand All @@ -57,27 +56,41 @@ export const ListsScreen = () => {
/>
</GroupedInsetListCard>
</View>
<View className="mt-6">
<GroupedInsetListCard>
{data && (
<SwipeableGroupProvider>
<Animated.FlatList
keyExtractor={keyExtractor}
itemLayoutAnimation={LinearTransition}
scrollEnabled={false}
data={data}
renderItem={ListItemCell}
ItemSeparatorComponent={ItemSeparatorComponent}
/>
</SwipeableGroupProvider>
)}
{isLoading && (
<View className="mt-1">
<LoadingIndicator />
</View>
)}
</GroupedInsetListCard>
</View>
<ListContext.Provider
value={useMemo(
() =>
data?.reduce(
(acc, list) => {
acc[list.id] = list
return acc
},
{} as Record<string, HonoApiClient.List_List_Get>,
) ?? {},
[data],
)}
>
<View className="mt-6">
<GroupedInsetListCard>
{lists.length > 0 && (
<SwipeableGroupProvider>
<Animated.FlatList
keyExtractor={keyExtractor}
itemLayoutAnimation={LinearTransition}
scrollEnabled={false}
data={lists}
renderItem={ListItemCell}
ItemSeparatorComponent={ItemSeparatorComponent}
/>
</SwipeableGroupProvider>
)}
{isLoading && lists.length === 0 && (
<View className="mt-1">
<LoadingIndicator />
</View>
)}
</GroupedInsetListCard>
</View>
</ListContext.Provider>
</SafeNavigationScrollView>
)
}
Expand All @@ -100,10 +113,14 @@ const ItemSeparatorComponent = () => {
)
}

const keyExtractor = (item: HonoApiClient.List_List_Get) => item.id
const keyExtractor = (item: ListModel) => item.id

const ListItemCell: ListRenderItem<HonoApiClient.List_List_Get> = ({ item: list }) => {
const ListItemCell: ListRenderItem<ListModel> = (props) => {
return <ListItemCellImpl {...props} />
}
const ListItemCellImpl: ListRenderItem<ListModel> = ({ item: list }) => {
const { title, description } = list
const listData = useContext(ListContext)[list.id]
return (
<SwipeableItem
rightActions={[
Expand Down Expand Up @@ -132,9 +149,11 @@ const ListItemCell: ListRenderItem<HonoApiClient.List_List_Get> = ({ item: list
>
{title}
</Text>
<Text className="text-secondary-label text-base" numberOfLines={4}>
{description}
</Text>
{!!description && (
<Text className="text-secondary-label text-base" numberOfLines={4}>
{description}
</Text>
)}
<View className="flex-row items-center gap-1">
{!!views[list.view]?.icon &&
createElement(views[list.view]!.icon, {
Expand All @@ -158,14 +177,14 @@ const ListItemCell: ListRenderItem<HonoApiClient.List_List_Get> = ({ item: list

<View className="flex-row items-center gap-1">
<UserAdd2CuteFiIcon height={16} width={16} color={accentColor} />
<Text className="text-secondary-label text-sm">{list.subscriptionCount || 0}</Text>
<Text className="text-secondary-label text-sm">{listData?.subscriptionCount || 0}</Text>
</View>

{!!list.purchaseAmount && (
{!!listData?.purchaseAmount && (
<View className="flex-row items-center gap-1">
<Wallet2CuteFiIcon height={16} width={16} color={accentColor} />
<Balance className="text-secondary-label text-sm">
{BigInt(list.purchaseAmount)}
{BigInt(listData.purchaseAmount)}
</Balance>
</View>
)}
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ActionSheetProvider } from "@expo/react-native-action-sheet"
import { jotaiStore } from "@follow/utils"
import { PortalProvider } from "@gorhom/portal"
import { ThemeProvider } from "@react-navigation/native"
Expand Down Expand Up @@ -30,7 +31,9 @@ export const RootProviders = ({ children }: { children: ReactNode }) => {
<QueryClientProvider client={queryClient}>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<GestureHandlerRootView>
<PortalProvider>{children}</PortalProvider>
<ActionSheetProvider>
<PortalProvider>{children}</PortalProvider>
</ActionSheetProvider>
</GestureHandlerRootView>
</ThemeProvider>
</QueryClientProvider>
Expand Down
124 changes: 112 additions & 12 deletions apps/mobile/src/screens/(modal)/list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useActionSheet } from "@expo/react-native-action-sheet"
import { FeedViewType } from "@follow/constants"
import { zodResolver } from "@hookform/resolvers/zod"
import { Stack, useLocalSearchParams } from "expo-router"
import { memo } from "react"
import { router, Stack, useLocalSearchParams } from "expo-router"
import { memo, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { View } from "react-native"
import { KeyboardAwareScrollView } from "react-native-keyboard-controller"
Expand All @@ -14,32 +15,58 @@ import {
import { FormProvider, useFormContext } from "@/src/components/ui/form/FormProvider"
import { FormLabel } from "@/src/components/ui/form/Label"
import { NumberField, TextField } from "@/src/components/ui/form/TextField"
import { GroupedInsetListCard } from "@/src/components/ui/grouped/GroupedList"
import {
GroupedInsetButtonCell,
GroupedInsetListCard,
} from "@/src/components/ui/grouped/GroupedList"
import { PowerIcon } from "@/src/icons/power"
import { getBizFetchErrorMessage } from "@/src/lib/api-fetch"
import { toast } from "@/src/lib/toast"
import { FeedViewSelector } from "@/src/modules/feed/view-selector"
import { getList } from "@/src/store/list/getters"
import { useList } from "@/src/store/list/hooks"
import type { ListModel } from "@/src/store/list/store"
import { listSyncServices } from "@/src/store/list/store"
import type { CreateListModel } from "@/src/store/list/types"
import { accentColor } from "@/src/theme/colors"

const listSchema = z.object({
title: z.string().min(1),
description: z.string().min(1),
image: z.string().url(),
description: z.string().nullable().optional(),
image: z
.string()
.url()
.nullable()
.optional()
.transform((val) => (val === "" ? null : val)),

fee: z.number().min(0),
view: z.nativeEnum(FeedViewType),
view: z.number().int(),
})

const defaultValues = {
title: "",
description: null,
image: null,
fee: 0,
view: FeedViewType.Articles,
} as ListModel
export default function ListScreen() {
const listId = useLocalSearchParams<{ id?: string }>().id

const list = useList(listId || "")
const form = useForm({
defaultValues: list,
defaultValues: list || defaultValues,
resolver: zodResolver(listSchema),
mode: "all",
})
const isEditing = !!listId
const { showActionSheetWithOptions } = useActionSheet()

return (
<FormProvider form={form}>
<KeyboardAwareScrollView className="bg-system-grouped-background flex-1 pb-safe">
<ScreenOptions title={list?.title} />
<ScreenOptions title={list?.title} listId={listId} />

<GroupedInsetListCard showSeparator={false} className="mt-6 px-3 py-6">
<Controller
Expand Down Expand Up @@ -88,11 +115,14 @@ export default function ListScreen() {
control={form.control}
render={({ field: { onChange, onBlur, ref, value } }) => (
<TextField
autoCapitalize="none"
label="Image"
wrapperClassName="mt-2"
placeholder="https://"
onBlur={onBlur}
onChangeText={onChange}
onChangeText={(val) => {
onChange(val === "" ? null : val)
}}
defaultValue={list?.image ?? ""}
value={value ?? ""}
ref={ref}
Expand Down Expand Up @@ -132,26 +162,96 @@ export default function ListScreen() {
/>
</View>
</GroupedInsetListCard>

{isEditing && (
<GroupedInsetListCard className="mt-6">
<GroupedInsetButtonCell
label="Delete"
style="destructive"
onPress={() => {
showActionSheetWithOptions(
{
options: ["Delete", "Cancel"],
cancelButtonIndex: 1,
destructiveButtonIndex: 0,
},
async (index) => {
if (index === 0) {
await listSyncServices.deleteList({ listId: listId! })
router.dismiss()
}
},
)
}}
/>
</GroupedInsetListCard>
)}
</KeyboardAwareScrollView>
</FormProvider>
)
}

interface ScreenOptionsProps {
title?: string
listId?: string
}
const ScreenOptions = memo(({ title }: ScreenOptionsProps) => {
const ScreenOptions = memo(({ title, listId }: ScreenOptionsProps) => {
const form = useFormContext()

const { isValid, isDirty } = form.formState

const isEditing = !!listId
const [isLoading, setIsLoading] = useState(false)

return (
<Stack.Screen
options={{
headerLeft: ModalHeaderCloseButton,
gestureEnabled: !form.formState.isDirty,
gestureEnabled: !isDirty,

headerRight: () => (
<FormProvider form={form}>
<ModalHeaderSubmitButton isValid onPress={() => {}} />
<ModalHeaderSubmitButton
isValid={isValid}
isLoading={isLoading}
onPress={form.handleSubmit((values) => {
if (!isEditing) {
setIsLoading(true)
listSyncServices
.createList({
list: values as CreateListModel,
})
.catch((error) => {
toast.error(getBizFetchErrorMessage(error))
console.error(error)
})
.finally(() => {
setIsLoading(false)
router.dismiss()
})
return
}
const list = getList(listId!)
if (!list) return
setIsLoading(true)
listSyncServices
.updateList({
listId: listId!,
list: {
...list,
...values,
},
})
.catch((error) => {
toast.error(getBizFetchErrorMessage(error))
console.error(error)
})
.finally(() => {
setIsLoading(false)
router.dismiss()
})
})}
/>
</FormProvider>
),

Expand Down
Loading

0 comments on commit 44c6fd0

Please sign in to comment.