feat(rn-list): manage list
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
527f6ac8b5
commit
e3baee3986
|
|
@ -1,3 +1,5 @@
|
|||
import { useTypeScriptHappyCallback } from "@follow/hooks"
|
||||
import { impactAsync, ImpactFeedbackStyle } from "expo-haptics"
|
||||
import { atom, useAtomValue, useSetAtom } from "jotai"
|
||||
import { selectAtom } from "jotai/utils"
|
||||
import * as React from "react"
|
||||
|
|
@ -17,6 +19,8 @@ interface SwipeableItemProps {
|
|||
leftActions?: Action[]
|
||||
rightActions?: Action[]
|
||||
disabled?: boolean
|
||||
|
||||
swipeRightToCallAction?: boolean
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -41,18 +45,22 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
})
|
||||
|
||||
const rectButtonWidth = 74
|
||||
|
||||
export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
||||
children,
|
||||
leftActions,
|
||||
rightActions,
|
||||
disabled,
|
||||
|
||||
swipeRightToCallAction,
|
||||
}) => {
|
||||
const [leftHaptic, setLeftHaptic] = React.useState(false)
|
||||
const [rightHaptic, setRightHaptic] = React.useState(false)
|
||||
const itemRef = React.useRef<Swipeable | null>(null)
|
||||
|
||||
const endDragCallerRef = React.useRef<() => void>(() => {})
|
||||
|
||||
const renderLeftActions = (progress: Animated.AnimatedInterpolation<number>) => {
|
||||
const width = leftActions?.length ? leftActions.length * 74 : 74
|
||||
const width = leftActions?.length ? leftActions.length * rectButtonWidth : rectButtonWidth
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -68,19 +76,9 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
|||
{leftActions?.map((action, index) => {
|
||||
const trans = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-74 * (leftHaptic ? (leftActions?.length ?? 1) : index + 1), 0],
|
||||
outputRange: [-rectButtonWidth * (leftActions?.length ?? 1), 0],
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
trans.addListener(({ value }) => {
|
||||
if (value >= (leftActions?.length === 1 ? 40 : 20)) {
|
||||
setLeftHaptic(true)
|
||||
} else {
|
||||
leftHaptic && setLeftHaptic(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
key={index}
|
||||
|
|
@ -88,8 +86,8 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
|||
styles.animatedContainer,
|
||||
{
|
||||
transform: [{ translateX: trans }],
|
||||
width: leftHaptic && index === 0 ? "100%" : 74,
|
||||
left: index * 74,
|
||||
width: rectButtonWidth,
|
||||
left: index * rectButtonWidth,
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
|
@ -116,12 +114,8 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
|||
}
|
||||
|
||||
const renderRightActions = (progress: Animated.AnimatedInterpolation<number>) => {
|
||||
const width = rightActions?.length ? rightActions.length * 74 : 74
|
||||
const width = rightActions?.length ? rightActions.length * rectButtonWidth : rectButtonWidth
|
||||
|
||||
const parallaxX = progress.interpolate({
|
||||
inputRange: [0, 1, 1.2],
|
||||
outputRange: [0, 0, 10],
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
|
|
@ -134,54 +128,18 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
|||
/>
|
||||
<Animated.View style={[styles.actionsWrapper, { width }]}>
|
||||
{rightActions?.map((action, index) => {
|
||||
const trans = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [74 * (rightHaptic ? (rightActions?.length ?? 1) : index + 1), 0],
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
trans.addListener(({ value }) => {
|
||||
if (value <= (rightActions?.length === 1 ? -40 : -20)) {
|
||||
setRightHaptic(true)
|
||||
} else {
|
||||
rightHaptic && setRightHaptic(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
<RightRectButton
|
||||
endDragCallerRef={endDragCallerRef}
|
||||
key={index}
|
||||
style={[
|
||||
styles.animatedContainer,
|
||||
{
|
||||
transform: [{ translateX: trans }],
|
||||
width: rightHaptic && index === 0 ? "100%" : 74,
|
||||
left: index * 74,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<RectButton
|
||||
style={[
|
||||
styles.actionContainer,
|
||||
{
|
||||
backgroundColor: action.backgroundColor ?? "#fff",
|
||||
},
|
||||
]}
|
||||
onPress={action.onPress}
|
||||
>
|
||||
{action.icon}
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.actionText,
|
||||
{ color: action.color ?? "#fff" },
|
||||
{ transform: [{ translateX: parallaxX }] },
|
||||
]}
|
||||
>
|
||||
{action.label}
|
||||
</Animated.Text>
|
||||
</RectButton>
|
||||
</Animated.View>
|
||||
index={index}
|
||||
action={action}
|
||||
length={rightActions?.length ?? 1}
|
||||
progress={progress}
|
||||
swipeRightToCallAction={
|
||||
swipeRightToCallAction && index === rightActions?.length - 1
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Animated.View>
|
||||
|
|
@ -218,26 +176,16 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
|||
rightThreshold={37}
|
||||
enableTrackpadTwoFingerGesture
|
||||
useNativeAnimations
|
||||
onEnded={(e: any) => {
|
||||
const { translationX } = e.nativeEvent
|
||||
if (
|
||||
leftHaptic &&
|
||||
translationX >= (leftActions?.length === 1 ? 100 : 60) * (leftActions?.length ?? 1)
|
||||
) {
|
||||
leftActions?.[0]?.onPress?.()
|
||||
onEnded={useTypeScriptHappyCallback(() => {
|
||||
if (swipeRightToCallAction && endDragCallerRef.current) {
|
||||
endDragCallerRef.current()
|
||||
}
|
||||
if (
|
||||
rightHaptic &&
|
||||
translationX <= (rightActions?.length === 1 ? -100 : -60) * (rightActions?.length ?? 1)
|
||||
) {
|
||||
rightActions?.[0]?.onPress?.()
|
||||
}
|
||||
}}
|
||||
}, [swipeRightToCallAction, endDragCallerRef])}
|
||||
renderLeftActions={leftActions?.length ? renderLeftActions : undefined}
|
||||
renderRightActions={rightActions?.length ? renderRightActions : undefined}
|
||||
overshootLeft={leftActions?.length ? leftActions?.length >= 1 : undefined}
|
||||
overshootRight={rightActions?.length ? rightActions?.length >= 1 : undefined}
|
||||
overshootFriction={10}
|
||||
overshootFriction={swipeRightToCallAction ? 1 : 10}
|
||||
>
|
||||
{children}
|
||||
</Swipeable>
|
||||
|
|
@ -258,3 +206,97 @@ export const SwipeableGroupProvider = ({ children }: { children: React.ReactNode
|
|||
|
||||
return <SwipeableGroupContext.Provider value={ctx}>{children}</SwipeableGroupContext.Provider>
|
||||
}
|
||||
|
||||
const rightActionThreshold = -100
|
||||
const RightRectButton = React.memo(
|
||||
({
|
||||
index,
|
||||
action,
|
||||
length = 1,
|
||||
progress,
|
||||
swipeRightToCallAction,
|
||||
endDragCallerRef,
|
||||
}: {
|
||||
progress: Animated.AnimatedInterpolation<number>
|
||||
index: number
|
||||
action: Action
|
||||
length: number
|
||||
swipeRightToCallAction?: boolean
|
||||
endDragCallerRef: React.MutableRefObject<() => void>
|
||||
}) => {
|
||||
const trans = React.useMemo(
|
||||
() =>
|
||||
progress.interpolate({
|
||||
inputRange: [0, 1, 1.2],
|
||||
outputRange: [rectButtonWidth * length, 0, -40],
|
||||
}),
|
||||
[progress, length],
|
||||
)
|
||||
const parallaxX = React.useMemo(
|
||||
() =>
|
||||
progress.interpolate({
|
||||
inputRange: [0, 1, 1.2],
|
||||
outputRange: [0, 0, 10],
|
||||
}),
|
||||
[progress],
|
||||
)
|
||||
|
||||
const hapticOnce = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!swipeRightToCallAction) return
|
||||
const id = trans.addListener(({ value }) => {
|
||||
if (value <= rightActionThreshold) {
|
||||
if (hapticOnce.current) return
|
||||
hapticOnce.current = true
|
||||
impactAsync(ImpactFeedbackStyle.Light)
|
||||
endDragCallerRef.current = () => {
|
||||
action.onPress?.()
|
||||
}
|
||||
} else {
|
||||
hapticOnce.current = false
|
||||
endDragCallerRef.current = () => {}
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
trans.removeListener(id)
|
||||
}
|
||||
}, [action, endDragCallerRef, swipeRightToCallAction, trans])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
key={index}
|
||||
style={[
|
||||
styles.animatedContainer,
|
||||
{
|
||||
transform: [{ translateX: trans }],
|
||||
width: rectButtonWidth,
|
||||
left: index * rectButtonWidth,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<RectButton
|
||||
style={[
|
||||
styles.actionContainer,
|
||||
{
|
||||
backgroundColor: action.backgroundColor ?? "#fff",
|
||||
},
|
||||
]}
|
||||
onPress={action.onPress}
|
||||
>
|
||||
{action.icon}
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.actionText,
|
||||
{ color: action.color ?? "#fff" },
|
||||
{ transform: [{ translateX: parallaxX }] },
|
||||
]}
|
||||
>
|
||||
{action.label}
|
||||
</Animated.Text>
|
||||
</RectButton>
|
||||
</Animated.View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const GroupedInsetListCard: FC<
|
|||
? React.Children.map(children, (child, index) => {
|
||||
const isLast = index === React.Children.count(children) - 1
|
||||
|
||||
if (child === null) return null
|
||||
const isNavigationLink =
|
||||
React.isValidElement(child) &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import type { ListModel } from "@/src/store/list/store"
|
|||
import { accentColor } from "@/src/theme/colors"
|
||||
|
||||
import { SwipeableGroupProvider, SwipeableItem } from "../../../components/common/SwipeableItem"
|
||||
import { useSettingsNavigation } from "../hooks"
|
||||
|
||||
const ListContext = createContext({} as Record<string, HonoApiClient.List_List_Get>)
|
||||
export const ListsScreen = () => {
|
||||
|
|
@ -122,13 +123,15 @@ const ListItemCell: ListRenderItem<ListModel> = (props) => {
|
|||
const ListItemCellImpl: ListRenderItem<ListModel> = ({ item: list }) => {
|
||||
const { title, description } = list
|
||||
const listData = useContext(ListContext)[list.id]
|
||||
const navigation = useSettingsNavigation()
|
||||
return (
|
||||
<SwipeableItem
|
||||
swipeRightToCallAction
|
||||
rightActions={[
|
||||
{
|
||||
label: "Manage",
|
||||
onPress: () => {
|
||||
router.push(`/manage-list?id=${list.id}`)
|
||||
navigation.navigate("ManageList", { id: list.id })
|
||||
},
|
||||
backgroundColor: accentColor,
|
||||
},
|
||||
|
|
@ -143,7 +146,7 @@ const ListItemCellImpl: ListRenderItem<ListModel> = ({ item: list }) => {
|
|||
>
|
||||
<ItemPressable
|
||||
className="flex-row p-4"
|
||||
onPress={() => router.push(`/manage-list?id=${list.id}`)}
|
||||
onPress={() => navigation.navigate("ManageList", { id: list.id })}
|
||||
>
|
||||
<View className="size-16 overflow-hidden rounded-lg">
|
||||
{list.image ? (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
import type { RouteProp } from "@react-navigation/native"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { router } from "expo-router"
|
||||
import type { MutableRefObject } from "react"
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Text, View } from "react-native"
|
||||
|
||||
import { ModalHeaderSubmitButton } from "@/src/components/common/ModalSharedComponents"
|
||||
import {
|
||||
NavigationBlurEffectHeader,
|
||||
SafeNavigationScrollView,
|
||||
} from "@/src/components/common/SafeNavigationScrollView"
|
||||
import {
|
||||
GroupedInsetListBaseCell,
|
||||
GroupedInsetListCard,
|
||||
GroupedInsetListSectionHeader,
|
||||
} from "@/src/components/ui/grouped/GroupedList"
|
||||
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
|
||||
import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
|
||||
import { CheckLineIcon } from "@/src/icons/check_line"
|
||||
import { getBizFetchErrorMessage } from "@/src/lib/api-fetch"
|
||||
import { toast } from "@/src/lib/toast"
|
||||
import { useFeed } from "@/src/store/feed/hooks"
|
||||
import { useList, usePrefetchOwnedLists } from "@/src/store/list/hooks"
|
||||
import { listSyncServices } from "@/src/store/list/store"
|
||||
import {
|
||||
useFeedSubscriptionByView,
|
||||
usePrefetchSubscription,
|
||||
useSortedFeedSubscriptionByAlphabet,
|
||||
} from "@/src/store/subscription/hooks"
|
||||
import { accentColor } from "@/src/theme/colors"
|
||||
|
||||
import type { SettingsStackParamList } from "../types"
|
||||
|
||||
const ManageListContext = createContext<{
|
||||
nextSelectedFeedIdRef: MutableRefObject<Set<string>>
|
||||
}>(null!)
|
||||
export const ManageListScreen = ({
|
||||
route,
|
||||
}: {
|
||||
route: RouteProp<SettingsStackParamList, "ManageList">
|
||||
}) => {
|
||||
const { id } = route.params
|
||||
|
||||
usePrefetchOwnedLists()
|
||||
const list = useList(id)
|
||||
|
||||
return (
|
||||
<SafeNavigationScrollView className="bg-system-grouped-background mt-6">
|
||||
<NavigationBlurEffectHeader title={`Manage List - ${list?.title}`} />
|
||||
|
||||
{!!list && <ListImpl id={list.id} />}
|
||||
</SafeNavigationScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const ListImpl: React.FC<{ id: string }> = ({ id }) => {
|
||||
const list = useList(id)!
|
||||
usePrefetchSubscription(list.view)
|
||||
|
||||
const subscriptionIds = useFeedSubscriptionByView(list.view)
|
||||
|
||||
const sortedSubscriptionIds = useSortedFeedSubscriptionByAlphabet(subscriptionIds)
|
||||
|
||||
const nextSelectedFeedIdRef = useRef(new Set<string>())
|
||||
const ctxValue = useMemo(() => ({ nextSelectedFeedIdRef }), [nextSelectedFeedIdRef])
|
||||
|
||||
const initOnceRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (initOnceRef.current) return
|
||||
initOnceRef.current = true
|
||||
|
||||
nextSelectedFeedIdRef.current = new Set(list.feedIds)
|
||||
}, [list.feedIds])
|
||||
|
||||
const addFeedsToFeedListMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
listSyncServices.addFeedsToFeedList({
|
||||
listId: id,
|
||||
feedIds: Array.from(nextSelectedFeedIdRef.current),
|
||||
}),
|
||||
})
|
||||
return (
|
||||
<ManageListContext.Provider value={ctxValue}>
|
||||
<NavigationBlurEffectHeader
|
||||
headerRight={() => (
|
||||
<ModalHeaderSubmitButton
|
||||
isLoading={addFeedsToFeedListMutation.isPending}
|
||||
isValid
|
||||
onPress={() => {
|
||||
addFeedsToFeedListMutation
|
||||
.mutateAsync()
|
||||
.then(() => {
|
||||
router.back()
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(getBizFetchErrorMessage(error))
|
||||
console.error(error)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<GroupedInsetListSectionHeader label="Select feeds to add to the current list" />
|
||||
<GroupedInsetListCard>
|
||||
{sortedSubscriptionIds.map((id) => (
|
||||
<FeedCell key={id} feedId={id} isSelected={list.feedIds.includes(id)} />
|
||||
))}
|
||||
</GroupedInsetListCard>
|
||||
</ManageListContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const FeedCell = (props: { feedId: string; isSelected: boolean }) => {
|
||||
const feed = useFeed(props.feedId)
|
||||
|
||||
const { nextSelectedFeedIdRef } = useContext(ManageListContext)
|
||||
|
||||
const [currentSelected, setCurrentSelected] = useState(props.isSelected)
|
||||
if (!feed) return null
|
||||
return (
|
||||
<ItemPressable
|
||||
onPress={() => {
|
||||
const has = nextSelectedFeedIdRef.current.has(feed.id)
|
||||
if (has) {
|
||||
nextSelectedFeedIdRef.current.delete(feed.id)
|
||||
} else {
|
||||
nextSelectedFeedIdRef.current.add(feed.id)
|
||||
}
|
||||
|
||||
setCurrentSelected(!has)
|
||||
}}
|
||||
>
|
||||
<GroupedInsetListBaseCell>
|
||||
<View className="flex-1 flex-row items-center gap-4">
|
||||
<View className="size-4 items-center justify-center">
|
||||
<View className="overflow-hidden rounded-lg">
|
||||
<FeedIcon feed={feed} size={24} />
|
||||
</View>
|
||||
</View>
|
||||
<Text className="flex-1" ellipsizeMode="middle" numberOfLines={1}>
|
||||
{feed?.title || "Untitled Feed"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="ml-2 flex size-4 shrink-0 items-center justify-center">
|
||||
{currentSelected && <CheckLineIcon color={accentColor} height={18} width={18} />}
|
||||
</View>
|
||||
</GroupedInsetListBaseCell>
|
||||
</ItemPressable>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { DataScreen } from "./Data"
|
|||
import { FeedsScreen } from "./Feeds"
|
||||
import { GeneralScreen } from "./General"
|
||||
import { ListsScreen } from "./Lists"
|
||||
import { ManageListScreen } from "./ManageList"
|
||||
import { NotificationsScreen } from "./Notifications"
|
||||
import { PrivacyScreen } from "./Privacy"
|
||||
import { ProfileScreen } from "./Profile"
|
||||
|
|
@ -25,5 +26,6 @@ export const SettingRoutes = (Stack: ReturnType<typeof createNativeStackNavigato
|
|||
<Stack.Screen key="Feeds" name="Feeds" component={FeedsScreen} />,
|
||||
<Stack.Screen key="Privacy" name="Privacy" component={PrivacyScreen} />,
|
||||
<Stack.Screen key="About" name="About" component={AboutScreen} />,
|
||||
<Stack.Screen key="ManageList" name="ManageList" component={ManageListScreen} />,
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,7 @@ export type SettingsStackParamList = {
|
|||
Feeds: undefined
|
||||
Privacy: undefined
|
||||
About: undefined
|
||||
ManageList: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { FeedSchema, InboxSchema } from "../database/schemas/types"
|
||||
import type { EntryModel } from "../store/entry/types"
|
||||
import type { FeedModel } from "../store/feed/types"
|
||||
import type { ListModel } from "../store/list/store"
|
||||
import type { SubscriptionModel } from "../store/subscription/store"
|
||||
import type { HonoApiClient } from "./types"
|
||||
|
|
@ -160,6 +161,20 @@ class Morph {
|
|||
read: false,
|
||||
}
|
||||
}
|
||||
|
||||
toFeed(data: HonoApiClient.Feed_Get["feed"]): FeedModel {
|
||||
return {
|
||||
id: data.id,
|
||||
title: data.title!,
|
||||
url: data.url,
|
||||
image: data.image!,
|
||||
description: data.description!,
|
||||
ownerUserId: data.ownerUserId!,
|
||||
errorAt: data.errorAt!,
|
||||
errorMessage: data.errorMessage!,
|
||||
siteUrl: data.siteUrl!,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const honoMorph = new Morph()
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ export namespace HonoApiClient {
|
|||
export type Entry_Post = ExtractData<typeof apiClient.entries.$post>
|
||||
export type Entry_Get = ExtractData<typeof apiClient.entries.$get>
|
||||
export type List_List_Get = ExtractData<typeof apiClient.lists.list.$get>[number]
|
||||
export type Feed_Get = ExtractData<typeof apiClient.feeds.$get>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,6 @@ export default function ModalLayout() {
|
|||
title: "List",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="manage-list"
|
||||
options={{
|
||||
title: "Manage List",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
import { View } from "react-native"
|
||||
|
||||
export default function ManageListScreen() {
|
||||
return <View />
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { honoMorph } from "@/src/morph/hono"
|
|||
import { storeDbMorph } from "@/src/morph/store-db"
|
||||
import { ListService } from "@/src/services/list"
|
||||
|
||||
import { feedActions } from "../feed/store"
|
||||
import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper"
|
||||
import { getList } from "./getters"
|
||||
import type { CreateListModel } from "./types"
|
||||
|
|
@ -158,6 +159,21 @@ class ListSyncServices {
|
|||
await apiClient.lists.$delete({ json: { listId: params.listId } })
|
||||
listActions.deleteList({ listId: params.listId })
|
||||
}
|
||||
|
||||
async addFeedsToFeedList(params: { listId: string; feedIds: string[] }) {
|
||||
const feeds = await apiClient.lists.feeds.$post({
|
||||
json: params,
|
||||
})
|
||||
const list = get().lists[params.listId]
|
||||
if (!list) return
|
||||
|
||||
feeds.data.forEach((feed) => {
|
||||
feedActions.upsertMany([honoMorph.toFeed(feed)])
|
||||
})
|
||||
listActions.upsertMany([
|
||||
{ ...list, feedIds: [...list.feedIds, ...feeds.data.map((feed) => feed.id)] },
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export const listSyncServices = new ListSyncServices()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ export const getSubscriptionByView = (view: FeedViewType): string[] => {
|
|||
.concat(Array.from(state.listIdByView[view]))
|
||||
}
|
||||
|
||||
export const getFeedSubscriptionByView = (view: FeedViewType): string[] => {
|
||||
const state = get()
|
||||
return Array.from(state.feedIdByView[view])
|
||||
}
|
||||
|
||||
export const getSubscriptionByCategory = (category: string): string[] => {
|
||||
const state = get()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ import { useCallback } from "react"
|
|||
import { getFeed } from "../feed/getter"
|
||||
import { getList } from "../list/getters"
|
||||
import { getUnreadCount } from "../unread/getter"
|
||||
import { getSubscription, getSubscriptionByCategory, getSubscriptionByView } from "./getter"
|
||||
import {
|
||||
getFeedSubscriptionByView,
|
||||
getSubscription,
|
||||
getSubscriptionByCategory,
|
||||
getSubscriptionByView,
|
||||
} from "./getter"
|
||||
import { subscriptionSyncService, useSubscriptionStore } from "./store"
|
||||
|
||||
export const usePrefetchSubscription = (view: FeedViewType) => {
|
||||
|
|
@ -42,6 +47,10 @@ export const useSubscriptionByView = (view: FeedViewType) => {
|
|||
return useSubscriptionStore(useCallback(() => getSubscriptionByView(view), [view]))
|
||||
}
|
||||
|
||||
export const useFeedSubscriptionByView = (view: FeedViewType) => {
|
||||
return useSubscriptionStore(useCallback(() => getFeedSubscriptionByView(view), [view]))
|
||||
}
|
||||
|
||||
export const useGroupedSubscription = (view: FeedViewType) => {
|
||||
return useSubscriptionStore(
|
||||
useCallback(
|
||||
|
|
@ -138,6 +147,19 @@ export const useSortedUngroupedSubscription = (
|
|||
)
|
||||
}
|
||||
|
||||
export const useSortedFeedSubscriptionByAlphabet = (ids: string[]) => {
|
||||
return useSubscriptionStore(
|
||||
useCallback(() => {
|
||||
return ids.sort((a, b) => {
|
||||
const leftFeed = getFeed(a)
|
||||
const rightFeed = getFeed(b)
|
||||
if (!leftFeed || !rightFeed) return 0
|
||||
return sortByAlphabet(leftFeed.title!, rightFeed.title!)
|
||||
})
|
||||
}, [ids]),
|
||||
)
|
||||
}
|
||||
|
||||
export const useSubscription = (id: string) => {
|
||||
return useSubscriptionStore((state) => {
|
||||
return state.data[id]
|
||||
|
|
|
|||
Loading…
Reference in New Issue