From 50fd58f82440aa1dab694bcf439bc79465754eec Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 13 Nov 2024 17:21:00 +0800 Subject: [PATCH] refactor: optimize feed and subscription logic with hooks (#1585) * perf: memo selector Signed-off-by: Innei * chore: remove hooks Signed-off-by: Innei * feat: rollback data Signed-off-by: Innei --------- Signed-off-by: Innei --- .../renderer/src/hooks/biz/useFeedActions.tsx | 94 ++++++++++-- .../src/modules/discover/inbox-list-form.tsx | 134 +++++++++-------- .../entry-column/layouts/TimelineTabs.tsx | 15 +- .../src/modules/feed-column/category.tsx | 51 ++++--- .../src/modules/feed-column/index.tsx | 25 ++-- .../renderer/src/modules/feed-column/list.tsx | 25 ++-- .../sort-by/SortByAlphabeticalList.tsx | 88 ++++++----- .../feed-column/sort-by/SortByUnreadList.tsx | 46 +++--- .../modules/settings/tabs/lists/modals.tsx | 26 +--- apps/renderer/src/store/entry/hooks.ts | 104 +++++++------ apps/renderer/src/store/feed/hooks.ts | 141 +++++------------- apps/renderer/src/store/feed/selector.ts | 46 +++++- apps/renderer/src/store/inbox/store.ts | 25 ++-- apps/renderer/src/store/list/hooks.ts | 13 +- .../src/store/subscription/getters.ts | 20 +++ apps/renderer/src/store/subscription/hooks.ts | 109 +++++++------- apps/renderer/src/store/subscription/index.ts | 1 + .../src/store/subscription/selector.ts | 57 +++++++ apps/renderer/src/store/subscription/store.ts | 17 --- 19 files changed, 599 insertions(+), 438 deletions(-) create mode 100644 apps/renderer/src/store/subscription/getters.ts diff --git a/apps/renderer/src/hooks/biz/useFeedActions.tsx b/apps/renderer/src/hooks/biz/useFeedActions.tsx index fcceeada3..f6c333152 100644 --- a/apps/renderer/src/hooks/biz/useFeedActions.tsx +++ b/apps/renderer/src/hooks/biz/useFeedActions.tsx @@ -4,27 +4,25 @@ import { IN_ELECTRON } from "@follow/shared/constants" import { env } from "@follow/shared/env" import { UrlBuilder } from "@follow/utils/url-builder" import { isBizId } from "@follow/utils/utils" -import { useMemo } from "react" +import { useMutation } from "@tanstack/react-query" +import { useMemo, useRef } from "react" import { useTranslation } from "react-i18next" +import { toast } from "sonner" import type { FollowMenuItem, MenuItemInput } from "~/atoms/context-menu" import { whoami } from "~/atoms/user" import { useModalStack } from "~/components/ui/modal/stacked/hooks" +import { apiClient } from "~/lib/api-fetch" import { useBoostModal } from "~/modules/boost/hooks" import { useFeedClaimModal } from "~/modules/claim" import { FeedForm } from "~/modules/discover/feed-form" import { InboxForm } from "~/modules/discover/inbox-form" import { ListForm } from "~/modules/discover/list-form" import { ListCreationModalContent } from "~/modules/settings/tabs/lists/modals" -import { - getFeedById, - useAddFeedToFeedList, - useFeedById, - useRemoveFeedFromFeedList, - useResetFeed, -} from "~/store/feed" +import { entries } from "~/queries/entries" +import { getFeedById, useFeedById } from "~/store/feed" import { useInboxById } from "~/store/inbox" -import { useListById, useOwnedListByView } from "~/store/list" +import { listActions, useListById, useOwnedListByView } from "~/store/list" import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription" import { useNavigateEntry } from "./useNavigateEntry" @@ -469,3 +467,81 @@ export const useInboxActions = ({ inboxId }: { inboxId: string }) => { return { items } } + +export const useAddFeedToFeedList = (options?: { + onSuccess?: () => void + onError?: () => void +}) => { + const { t } = useTranslation("settings") + return useMutation({ + mutationFn: async ( + payload: { feedId: string; listId: string } | { feedIds: string[]; listId: string }, + ) => { + const feeds = await apiClient.lists.feeds.$post({ + json: payload, + }) + + feeds.data.forEach((feed) => listActions.addFeedToFeedList(payload.listId, feed)) + }, + onSuccess: () => { + toast.success(t("lists.feeds.add.success")) + + options?.onSuccess?.() + }, + async onError() { + toast.error(t("lists.feeds.add.error")) + options?.onError?.() + }, + }) +} + +export const useRemoveFeedFromFeedList = (options?: { + onSuccess: () => void + onError: () => void +}) => { + const { t } = useTranslation("settings") + return useMutation({ + mutationFn: async (payload: { feedId: string; listId: string }) => { + listActions.removeFeedFromFeedList(payload.listId, payload.feedId) + await apiClient.lists.feeds.$delete({ + json: { + listId: payload.listId, + feedId: payload.feedId, + }, + }) + }, + onSuccess: () => { + toast.success(t("lists.feeds.delete.success")) + options?.onSuccess?.() + }, + async onError() { + toast.error(t("lists.feeds.delete.error")) + options?.onError?.() + }, + }) +} + +export const useResetFeed = () => { + const { t } = useTranslation() + const toastIDRef = useRef(null) + + return useMutation({ + mutationFn: async (feedId: string) => { + toastIDRef.current = toast.loading(t("sidebar.feed_actions.resetting_feed")) + await apiClient.feeds.reset.$get({ query: { id: feedId } }) + }, + onSuccess: (_, feedId) => { + entries.entries({ feedId }).invalidateRoot() + toast.success( + t("sidebar.feed_actions.reset_feed_success"), + toastIDRef.current ? { id: toastIDRef.current } : undefined, + ) + }, + onError: () => { + toast.error( + t("sidebar.feed_actions.reset_feed_error"), + toastIDRef.current ? { id: toastIDRef.current } : undefined, + ) + }, + }) +} diff --git a/apps/renderer/src/modules/discover/inbox-list-form.tsx b/apps/renderer/src/modules/discover/inbox-list-form.tsx index a4a6fd4db..83713615e 100644 --- a/apps/renderer/src/modules/discover/inbox-list-form.tsx +++ b/apps/renderer/src/modules/discover/inbox-list-form.tsx @@ -11,18 +11,19 @@ import { import { FeedViewType, UserRole } from "@follow/constants" import { env } from "@follow/shared/env" import { useMutation } from "@tanstack/react-query" +import { memo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { useEventCallback } from "usehooks-ts" import { useUserRole } from "~/atoms/user" import { CopyButton } from "~/components/ui/code-highlighter" -import { useModalStack } from "~/components/ui/modal/stacked/hooks" +import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks" import { MAX_TRIAL_USER_INBOX_SUBSCRIPTION } from "~/constants/limit" import { CustomSafeError } from "~/errors/CustomSafeError" import { createErrorToaster } from "~/lib/error-parser" import { useInboxList } from "~/queries/inboxes" -import { inboxActions } from "~/store/inbox" +import { inboxActions, useInboxById } from "~/store/inbox" import { subscriptionActions, useInboxSubscriptionCount } from "~/store/subscription" import { useActivationModal } from "../activation" @@ -97,68 +98,7 @@ export function DiscoverInboxList() { ) : ( - inboxes.data?.map((inbox) => ( - - {inbox.id} - -
- - {inbox.id} - {env.VITE_INBOXES_EMAIL} - - -
-
- {inbox.title} - -
- **** - -
-
- - - present({ - title: t("discover.inbox_destroy_confirm"), - content: ({ dismiss }) => ( - { - inboxes.refetch() - dismiss() - }} - /> - ), - }) - } - > - - - { - present({ - title: t("sidebar.feed_actions.edit_inbox"), - content: ({ dismiss }) => ( - - ), - }) - }} - > - - - -
- )) + inboxes.data?.map((inbox) => ) )} @@ -190,8 +130,9 @@ export function DiscoverInboxList() { ) } -const ConfirmDestroyModalContent = ({ id, onSuccess }: { id: string; onSuccess: () => void }) => { +const ConfirmDestroyModalContent = ({ id }: { id: string }) => { const { t } = useTranslation() + const { dismiss } = useCurrentModal() const mutationDestroy = useMutation({ mutationFn: async (id: string) => { @@ -200,7 +141,9 @@ const ConfirmDestroyModalContent = ({ id, onSuccess }: { id: string; onSuccess: onSuccess: () => { subscriptionActions.fetchByView(FeedViewType.Articles) toast.success(t("discover.inbox_destroy_success")) - onSuccess() + }, + onMutate: () => { + dismiss() }, onError: createErrorToaster(t("discover.inbox_destroy_error")), }) @@ -219,3 +162,62 @@ const ConfirmDestroyModalContent = ({ id, onSuccess }: { id: string; onSuccess: ) } + +const Row = memo(({ id }: { id: string }) => { + const { t } = useTranslation() + const { present } = useModalStack() + const inbox = useInboxById(id) + if (!inbox) return null + return ( + + {inbox.id} + +
+ + {inbox.id} + {env.VITE_INBOXES_EMAIL} + + +
+
+ {inbox.title} + +
+ **** + +
+
+ + + present({ + title: t("discover.inbox_destroy_confirm"), + content: () => , + }) + } + > + + + { + present({ + title: t("sidebar.feed_actions.edit_inbox"), + content: ({ dismiss }) => , + }) + }} + > + + + +
+ ) +}) diff --git a/apps/renderer/src/modules/entry-column/layouts/TimelineTabs.tsx b/apps/renderer/src/modules/entry-column/layouts/TimelineTabs.tsx index 13f441110..4bedf0abf 100644 --- a/apps/renderer/src/modules/entry-column/layouts/TimelineTabs.tsx +++ b/apps/renderer/src/modules/entry-column/layouts/TimelineTabs.tsx @@ -1,4 +1,5 @@ import { Tabs, TabsList, TabsTrigger } from "@follow/components/ui/tabs/index.jsx" +import { useCallback } from "react" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { useRouteParams } from "~/hooks/biz/useRouteParams" @@ -9,11 +10,17 @@ export const TimelineTabs = () => { const routerParams = useRouteParams() const { view, listId, inboxId } = routerParams - const listsData = useSubscriptionStore((state) => - state.feedIdByView[view].map((id) => state.data[id]).filter((s) => "listId" in s), + const listsData = useSubscriptionStore( + useCallback( + (state) => state.feedIdByView[view].map((id) => state.data[id]).filter((s) => "listId" in s), + [view], + ), ) - const inboxData = useSubscriptionStore((state) => - state.feedIdByView[view].map((id) => state.data[id]).filter((s) => "inboxId" in s), + const inboxData = useSubscriptionStore( + useCallback( + (state) => state.feedIdByView[view].map((id) => state.data[id]).filter((s) => "inboxId" in s), + [view], + ), ) const hasData = listsData.length > 0 || inboxData.length > 0 diff --git a/apps/renderer/src/modules/feed-column/category.tsx b/apps/renderer/src/modules/feed-column/category.tsx index 0e7112dd2..f05476757 100644 --- a/apps/renderer/src/modules/feed-column/category.tsx +++ b/apps/renderer/src/modules/feed-column/category.tsx @@ -18,10 +18,11 @@ import { useOnClickOutside } from "usehooks-ts" import type { MenuItemInput } from "~/atoms/context-menu" import { useShowContextMenu } from "~/atoms/context-menu" import { ROUTE_FEED_IN_FOLDER } from "~/constants" +import { useAddFeedToFeedList } from "~/hooks/biz/useFeedActions" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { getRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { createErrorToaster } from "~/lib/error-parser" -import { getPreferredTitle, useAddFeedToFeedList, useFeedStore } from "~/store/feed" +import { getPreferredTitle, useFeedStore } from "~/store/feed" import { useOwnedListByView } from "~/store/list" import { subscriptionActions, @@ -48,8 +49,8 @@ interface FeedCategoryProps { function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCategoryProps) { const { t } = useTranslation() - const sortByUnreadFeedList = useFeedUnreadStore((state) => - ids.sort((a, b) => (state.data[b] || 0) - (state.data[a] || 0)), + const sortByUnreadFeedList = useFeedUnreadStore( + useCallback((state) => ids.sort((a, b) => (state.data[b] || 0) - (state.data[a] || 0)), [ids]), ) const navigate = useNavigateEntry() @@ -126,8 +127,8 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego } } - const unread = useFeedUnreadStore((state) => - ids.reduce((acc, feedId) => (state.data[feedId] || 0) + acc, 0), + const unread = useFeedUnreadStore( + useCallback((state) => ids.reduce((acc, feedId) => (state.data[feedId] || 0) + acc, 0), [ids]), ) const isActive = useRouteParamsSelector( @@ -452,18 +453,23 @@ const SortedFeedItems = (props: SortListProps) => { const SortByAlphabeticalList = (props: SortListProps) => { const { ids, showCollapse, view } = props const isDesc = useFeedListSortSelector((s) => s.order === "desc") - const sortedFeedList = useFeedStore((state) => { - const res = ids.sort((a, b) => { - const feedTitleA = getPreferredTitle(state.feeds[a]) || "" - const feedTitleB = getPreferredTitle(state.feeds[b]) || "" - return sortByAlphabet(feedTitleA, feedTitleB) - }) + const sortedFeedList = useFeedStore( + useCallback( + (state) => { + const res = ids.sort((a, b) => { + const feedTitleA = getPreferredTitle(state.feeds[a]) || "" + const feedTitleB = getPreferredTitle(state.feeds[b]) || "" + return sortByAlphabet(feedTitleA, feedTitleB) + }) - if (isDesc) { - return res - } - return res.reverse() - }) + if (isDesc) { + return res + } + return res.reverse() + }, + [ids, isDesc], + ), + ) return ( {sortedFeedList.map((feedId) => ( @@ -479,10 +485,15 @@ const SortByAlphabeticalList = (props: SortListProps) => { } const SortByUnreadList = ({ ids, showCollapse, view }: SortListProps) => { const isDesc = useFeedListSortSelector((s) => s.order === "desc") - const sortByUnreadFeedList = useFeedUnreadStore((state) => { - const res = ids.sort((a, b) => (state.data[b] || 0) - (state.data[a] || 0)) - return isDesc ? res : res.reverse() - }) + const sortByUnreadFeedList = useFeedUnreadStore( + useCallback( + (state) => { + const res = ids.sort((a, b) => (state.data[b] || 0) - (state.data[a] || 0)) + return isDesc ? res : res.reverse() + }, + [ids, isDesc], + ), + ) return ( diff --git a/apps/renderer/src/modules/feed-column/index.tsx b/apps/renderer/src/modules/feed-column/index.tsx index 5b19578dc..74cdde794 100644 --- a/apps/renderer/src/modules/feed-column/index.tsx +++ b/apps/renderer/src/modules/feed-column/index.tsx @@ -51,17 +51,22 @@ const useBackHome = (active: number) => { const useUnreadByView = () => { useAuthQuery(Queries.subscription.byView()) const idByView = useSubscriptionStore((state) => state.feedIdByView) - const totalUnread = useFeedUnreadStore((state) => { - const unread = {} as Record + const totalUnread = useFeedUnreadStore( + useCallback( + (state) => { + const unread = {} as Record - for (const view in idByView) { - unread[view] = idByView[view].reduce( - (acc: number, feedId: string) => acc + (state.data[feedId] || 0), - 0, - ) - } - return unread - }) + for (const view in idByView) { + unread[view] = idByView[view].reduce( + (acc: number, feedId: string) => acc + (state.data[feedId] || 0), + 0, + ) + } + return unread + }, + [idByView], + ), + ) return totalUnread } diff --git a/apps/renderer/src/modules/feed-column/list.tsx b/apps/renderer/src/modules/feed-column/list.tsx index 22ba219b6..343596050 100644 --- a/apps/renderer/src/modules/feed-column/list.tsx +++ b/apps/renderer/src/modules/feed-column/list.tsx @@ -6,7 +6,7 @@ import { stopPropagation } from "@follow/utils/dom" import { cn } from "@follow/utils/utils" import * as HoverCard from "@radix-ui/react-hover-card" import { AnimatePresence, m } from "framer-motion" -import { memo, useMemo, useRef, useState } from "react" +import { memo, useCallback, useMemo, useRef, useState } from "react" import { isHotkeyPressed } from "react-hotkeys-hook" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" @@ -315,16 +315,21 @@ const ListHeader = ({ view }: { view: number }) => { const expansion = Object.values(categoryOpenStateData).every((value) => value === true) useUpdateUnreadCount() - const totalUnread = useFeedUnreadStore((state) => { - let unread = 0 + const totalUnread = useFeedUnreadStore( + useCallback( + (state) => { + let unread = 0 - for (const category in feedsData) { - for (const feedId of feedsData[category]) { - unread += state.data[feedId] || 0 - } - } - return unread - }) + for (const category in feedsData) { + for (const feedId of feedsData[category]) { + unread += state.data[feedId] || 0 + } + } + return unread + }, + [feedsData], + ), + ) const navigateEntry = useNavigateEntry() diff --git a/apps/renderer/src/modules/feed-column/sort-by/SortByAlphabeticalList.tsx b/apps/renderer/src/modules/feed-column/sort-by/SortByAlphabeticalList.tsx index 7097c4df4..a39f3c5da 100644 --- a/apps/renderer/src/modules/feed-column/sort-by/SortByAlphabeticalList.tsx +++ b/apps/renderer/src/modules/feed-column/sort-by/SortByAlphabeticalList.tsx @@ -1,5 +1,5 @@ import { sortByAlphabet } from "@follow/utils/utils" -import { Fragment } from "react" +import { Fragment, useCallback } from "react" import { INBOX_PREFIX_ID } from "~/constants" import { getPreferredTitle, useFeedStore } from "~/store/feed" @@ -15,45 +15,55 @@ export const SortByAlphabeticalFeedList = ({ data, categoryOpenStateData, }: FeedListProps) => { - const feedId2CategoryMap = useSubscriptionStore((state) => { - const map = {} as Record - for (const categoryName in data) { - const feedId = data[categoryName][0] - if (!feedId) { - continue - } - const subscription = state.data[feedId] - if (!subscription) { - continue - } - if (subscription.category) { - map[feedId] = subscription.category - } - } - return map - }) - const categoryName2RealDisplayNameMap = useFeedStore((state) => { - const map = {} as Record - for (const categoryName in data) { - const feedId = data[categoryName][0] + const feedId2CategoryMap = useSubscriptionStore( + useCallback( + (state) => { + const map = {} as Record + for (const categoryName in data) { + const feedId = data[categoryName][0] + if (!feedId) { + continue + } + const subscription = state.data[feedId] + if (!subscription) { + continue + } + if (subscription.category) { + map[feedId] = subscription.category + } + } + return map + }, + [data], + ), + ) + const categoryName2RealDisplayNameMap = useFeedStore( + useCallback( + (state) => { + const map = {} as Record + for (const categoryName in data) { + const feedId = data[categoryName][0] - if (!feedId) { - continue - } - const feed = state.feeds[feedId] - if (!feed) { - continue - } - const hascategoryNameNotDefault = !!feedId2CategoryMap[feedId] - const isSingle = data[categoryName].length === 1 - if (!isSingle || hascategoryNameNotDefault) { - map[categoryName] = categoryName - } else { - map[categoryName] = getPreferredTitle(feed)! - } - } - return map - }) + if (!feedId) { + continue + } + const feed = state.feeds[feedId] + if (!feed) { + continue + } + const hascategoryNameNotDefault = !!feedId2CategoryMap[feedId] + const isSingle = data[categoryName].length === 1 + if (!isSingle || hascategoryNameNotDefault) { + map[categoryName] = categoryName + } else { + map[categoryName] = getPreferredTitle(feed)! + } + } + return map + }, + [data, feedId2CategoryMap], + ), + ) const isDesc = useFeedListSortSelector((s) => s.order === "desc") diff --git a/apps/renderer/src/modules/feed-column/sort-by/SortByUnreadList.tsx b/apps/renderer/src/modules/feed-column/sort-by/SortByUnreadList.tsx index 06bf5fdd2..b5ce07b6b 100644 --- a/apps/renderer/src/modules/feed-column/sort-by/SortByUnreadList.tsx +++ b/apps/renderer/src/modules/feed-column/sort-by/SortByUnreadList.tsx @@ -1,4 +1,4 @@ -import { Fragment } from "react" +import { Fragment, useCallback } from "react" import { useFeedUnreadStore } from "~/store/unread" @@ -9,26 +9,34 @@ import type { FeedListProps } from "./types" export const SortByUnreadFeedList = ({ view, data, categoryOpenStateData }: FeedListProps) => { const isDesc = useFeedListSortSelector((s) => s.order === "desc") - const sortedByUnread = useFeedUnreadStore((state) => { - const sortedList = [] as [string, string[]][] - const folderUnread = {} as Record - // Calc total unread count for each folder - for (const category in data) { - folderUnread[category] = data[category].reduce((acc, cur) => (state.data[cur] || 0) + acc, 0) - } + const sortedByUnread = useFeedUnreadStore( + useCallback( + (state) => { + const sortedList = [] as [string, string[]][] + const folderUnread = {} as Record + // Calc total unread count for each folder + for (const category in data) { + folderUnread[category] = data[category].reduce( + (acc, cur) => (state.data[cur] || 0) + acc, + 0, + ) + } - // Sort by unread count - Object.keys(folderUnread) - .sort((a, b) => folderUnread[b] - folderUnread[a]) - .forEach((key) => { - sortedList.push([key, data[key]]) - }) + // Sort by unread count + Object.keys(folderUnread) + .sort((a, b) => folderUnread[b] - folderUnread[a]) + .forEach((key) => { + sortedList.push([key, data[key]]) + }) - if (!isDesc) { - sortedList.reverse() - } - return sortedList - }) + if (!isDesc) { + sortedList.reverse() + } + return sortedList + }, + [data, isDesc], + ), + ) return ( diff --git a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx index fbfb48fc0..f7998285f 100644 --- a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx +++ b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx @@ -35,20 +35,16 @@ import { z } from "zod" import type { Suggestion } from "~/components/ui/auto-completion" import { Autocomplete } from "~/components/ui/auto-completion" import { useCurrentModal } from "~/components/ui/modal/stacked/hooks" +import { useAddFeedToFeedList, useRemoveFeedFromFeedList } from "~/hooks/biz/useFeedActions" import { apiClient } from "~/lib/api-fetch" import { createErrorToaster } from "~/lib/error-parser" import { FeedCertification } from "~/modules/feed/feed-certification" import { FeedIcon } from "~/modules/feed/feed-icon" import { ViewSelectorRadioGroup } from "~/modules/shared/ViewSelectorRadioGroup" import { Queries } from "~/queries" -import { - getFeedById, - useAddFeedToFeedList, - useFeedById, - useRemoveFeedFromFeedList, -} from "~/store/feed" +import { useFeedById } from "~/store/feed" import { useListById } from "~/store/list" -import { subscriptionActions, useSubscriptionStore } from "~/store/subscription" +import { subscriptionActions, useAllFeeds } from "~/store/subscription" const formSchema = z.object({ view: z.string(), @@ -224,21 +220,7 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => { }, }) - const allFeeds = useSubscriptionStore((store) => { - const feedInfo = [] as { title: string; id: string }[] - - const allSubscriptions = Object.values(store.feedIdByView).flat() - - for (const feedId of allSubscriptions) { - const subscription = store.data[feedId] - const feed = getFeedById(feedId) - if (feed && feed.type === "feed") { - feedInfo.push({ title: subscription.title || feed.title || "", id: feed.id }) - } - } - return feedInfo - }) - + const allFeeds = useAllFeeds() const autocompleteSuggestions: Suggestion[] = useMemo(() => { return allFeeds .filter((feed) => !list?.feedIds?.includes(feed.id)) diff --git a/apps/renderer/src/store/entry/hooks.ts b/apps/renderer/src/store/entry/hooks.ts index 02772997d..a686e98d7 100644 --- a/apps/renderer/src/store/entry/hooks.ts +++ b/apps/renderer/src/store/entry/hooks.ts @@ -1,6 +1,6 @@ import type { FeedViewType } from "@follow/constants" import type { EntryReadHistoriesModel } from "@follow/shared/hono" -import { useShallow } from "zustand/react/shallow" +import { useCallback } from "react" import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER } from "~/constants" @@ -14,79 +14,85 @@ export const useEntry = ( selector?: (state: FlatEntryModel) => T, ): T | null => useEntryStore( - useShallow((state) => { - if (!entryId) return null - const data = state.flatMapEntries[entryId] + useCallback( + (state) => { + if (!entryId) return null + const data = state.flatMapEntries[entryId] - if (!data) return null + if (!data) return null - return selector ? selector(data) : (data as T) - }), + return selector ? selector(data) : (data as T) + }, + [entryId, selector], + ), ) // feedId: single feedId, multiple feedId joint by `,`, and `collections` export const useEntryIdsByFeedId = (feedId: string, filter?: EntryFilter) => useEntryStore( - useShallow((state) => { - if (typeof feedId !== "string") return [] - const isMultiple = feedId.includes(",") + useCallback( + (state) => { + if (typeof feedId !== "string") return [] + const isMultiple = feedId.includes(",") - const isInFolder = feedId.startsWith(ROUTE_FEED_IN_FOLDER) + const isInFolder = feedId.startsWith(ROUTE_FEED_IN_FOLDER) - if (isMultiple) { - const feedIds = feedId.split(",") - const result = [] as string[] - for (const id of feedIds) { - result.push(...getSingle(id)) - } - return result - } else if (feedId === FEED_COLLECTION_LIST) { - const result = [] as string[] - state.starIds.forEach((entryId) => { - if (getEntryIsInView(entryId)?.toString() === filter?.view?.toString()) { - result.push(entryId) - } - }) - - return result - } else if (isInFolder) { - // please use `useEntryIdsByFolderName` instead - return [] - } else { - return getSingle(feedId) - } - - function getSingle(feedId: string) { - const data = state.entries[feedId] || [] - if (filter?.unread) { + if (isMultiple) { + const feedIds = feedId.split(",") const result = [] as string[] - for (const entryId of data) { - const entry = state.flatMapEntries[entryId] - if (!entry?.read) { - result.push(entryId) - } + for (const id of feedIds) { + result.push(...getSingle(id)) } return result + } else if (feedId === FEED_COLLECTION_LIST) { + const result = [] as string[] + state.starIds.forEach((entryId) => { + if (getEntryIsInView(entryId)?.toString() === filter?.view?.toString()) { + result.push(entryId) + } + }) + + return result + } else if (isInFolder) { + // please use `useEntryIdsByFolderName` instead + return [] + } else { + return getSingle(feedId) } - return data - } - }), + + function getSingle(feedId: string) { + const data = state.entries[feedId] || [] + if (filter?.unread) { + const result = [] as string[] + for (const entryId of data) { + const entry = state.flatMapEntries[entryId] + if (!entry?.read) { + result.push(entryId) + } + } + return result + } + return data + } + }, + [feedId, filter?.unread, filter?.view], + ), ) export const useEntryIdsByView = (view: FeedViewType, filter?: EntryFilter) => { const feedIds = useFeedIdByView(view) - return useEntryStore(useShallow(() => getFilteredFeedIds(feedIds, filter))) + return useEntryStore(useCallback(() => getFilteredFeedIds(feedIds, filter), [feedIds, filter])) } export const useEntryIdsByFeedIds = (feedIds: string[], filter: EntryFilter = {}) => useEntryStore( - useShallow(() => { + useCallback(() => { if (!feedIds) return null if (!Array.isArray(feedIds)) return null return getFilteredFeedIds(feedIds, filter) - }), + }, [feedIds, filter]), ) export const useEntryIdsByFeedIdOrView = ( feedIdOrView: string | string[] | FeedViewType, @@ -107,4 +113,4 @@ export const useEntryIdsByFeedIdOrView = ( export const useEntryReadHistory = ( entryId: string, ): Omit | null => - useEntryStore(useShallow((state) => state.readHistory[entryId])) + useEntryStore((state) => state.readHistory[entryId]) diff --git a/apps/renderer/src/store/feed/hooks.ts b/apps/renderer/src/store/feed/hooks.ts index 7dfef90e6..0ffe87b58 100644 --- a/apps/renderer/src/store/feed/hooks.ts +++ b/apps/renderer/src/store/feed/hooks.ts @@ -1,47 +1,50 @@ import { views } from "@follow/constants" import type { FeedModel, FeedOrListRespModel, InboxModel, ListModel } from "@follow/models/types" -import { useMutation } from "@tanstack/react-query" -import { useRef } from "react" +import { useCallback } from "react" import { useTranslation } from "react-i18next" -import { toast } from "sonner" -import { useShallow } from "zustand/react/shallow" import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER, ROUTE_FEED_PENDING } from "~/constants" import { useRouteParams } from "~/hooks/biz/useRouteParams" -import { apiClient } from "~/lib/api-fetch" -import { entries } from "~/queries/entries" import { useInboxStore } from "../inbox" -import { listActions, useListStore } from "../list" +import { useListStore } from "../list" +import { + feedByIdOrUrlSelector, + feedByIdSelector, + feedByIdSelectorWithTransform, + feedByIdWithTransformSelector, + inboxByIdSelectorWithTransform, + listByIdSelectorWithTransform, +} from "./selector" import { getPreferredTitle, useFeedStore } from "./store" import type { FeedQueryParams } from "./types" export function useFeedById(feedId: Nullable): FeedModel | null export function useFeedById(feedId: Nullable, selector: (feed: FeedModel) => T): T | null -export function useFeedById(feedId: Nullable, selector?: (feed: FeedModel) => T) { - return useFeedStore((state) => { - const feed = feedId ? state.feeds[feedId] : null - return selector ? feed && selector(feed) : feed - }) +export function useFeedById(feedId: Nullable, transform?: (feed: FeedModel) => T) { + return useFeedStore( + useCallback( + (state) => + transform + ? feedByIdWithTransformSelector(feedId, transform)(state) + : feedByIdSelector(feedId)(state), + [feedId, transform], + ), + ) } export const useFeedByIdOrUrl = (feed: FeedQueryParams) => - useFeedStore((state) => { - if (feed.id) { - return state.feeds[feed.id] - } - if (feed.url) { - return Object.values(state.feeds).find((f) => f.type === "feed" && f.url === feed.url) || null - } - return null - }) + useFeedStore(useCallback((state) => feedByIdOrUrlSelector(feed)(state), [feed])) export const useFeedByIdSelector = ( feedId: Nullable, selector: (feed: FeedOrListRespModel) => T, ) => useFeedStore( - useShallow((state) => (feedId && state.feeds[feedId] ? selector(state.feeds[feedId]) : null)), + useCallback( + (state) => feedByIdSelectorWithTransform(feedId, selector)(state), + [feedId, selector], + ), ) export const useListByIdSelector = ( @@ -49,7 +52,10 @@ export const useListByIdSelector = ( selector: (list: ListModel) => T, ) => useListStore( - useShallow((state) => (listId && state.lists[listId] ? selector(state.lists[listId]) : null)), + useCallback( + (state) => listByIdSelectorWithTransform(listId, selector)(state), + [listId, selector], + ), ) export const useInboxByIdSelector = ( @@ -57,8 +63,9 @@ export const useInboxByIdSelector = ( selector: (inbox: InboxModel) => T, ) => useInboxStore( - useShallow((state) => - inboxId && state.inboxes[inboxId] ? selector(state.inboxes[inboxId]) : null, + useCallback( + (state) => inboxByIdSelectorWithTransform(inboxId, selector)(state), + [inboxId, selector], ), ) @@ -67,9 +74,9 @@ export const useFeedHeaderTitle = () => { const { feedId: currentFeedId, view, listId, inboxId } = useRouteParams() - const listTitle = useListByIdSelector(listId, (list) => getPreferredTitle(list)) - const inboxTitle = useInboxByIdSelector(inboxId, (inbox) => getPreferredTitle(inbox)) - const feedTitle = useFeedByIdSelector(currentFeedId, (feed) => getPreferredTitle(feed)) + const listTitle = useListByIdSelector(listId, getPreferredTitle) + const inboxTitle = useInboxByIdSelector(inboxId, getPreferredTitle) + const feedTitle = useFeedByIdSelector(currentFeedId, getPreferredTitle) switch (currentFeedId) { case ROUTE_FEED_PENDING: { @@ -86,81 +93,3 @@ export const useFeedHeaderTitle = () => { } } } - -export const useAddFeedToFeedList = (options?: { - onSuccess?: () => void - onError?: () => void -}) => { - const { t } = useTranslation("settings") - return useMutation({ - mutationFn: async ( - payload: { feedId: string; listId: string } | { feedIds: string[]; listId: string }, - ) => { - const feeds = await apiClient.lists.feeds.$post({ - json: payload, - }) - - feeds.data.forEach((feed) => listActions.addFeedToFeedList(payload.listId, feed)) - }, - onSuccess: () => { - toast.success(t("lists.feeds.add.success")) - - options?.onSuccess?.() - }, - async onError() { - toast.error(t("lists.feeds.add.error")) - options?.onError?.() - }, - }) -} - -export const useRemoveFeedFromFeedList = (options?: { - onSuccess: () => void - onError: () => void -}) => { - const { t } = useTranslation("settings") - return useMutation({ - mutationFn: async (payload: { feedId: string; listId: string }) => { - listActions.removeFeedFromFeedList(payload.listId, payload.feedId) - await apiClient.lists.feeds.$delete({ - json: { - listId: payload.listId, - feedId: payload.feedId, - }, - }) - }, - onSuccess: () => { - toast.success(t("lists.feeds.delete.success")) - options?.onSuccess?.() - }, - async onError() { - toast.error(t("lists.feeds.delete.error")) - options?.onError?.() - }, - }) -} - -export const useResetFeed = () => { - const { t } = useTranslation() - const toastIDRef = useRef(null) - - return useMutation({ - mutationFn: async (feedId: string) => { - toastIDRef.current = toast.loading(t("sidebar.feed_actions.resetting_feed")) - await apiClient.feeds.reset.$get({ query: { id: feedId } }) - }, - onSuccess: (_, feedId) => { - entries.entries({ feedId }).invalidateRoot() - toast.success( - t("sidebar.feed_actions.reset_feed_success"), - toastIDRef.current ? { id: toastIDRef.current } : undefined, - ) - }, - onError: () => { - toast.error( - t("sidebar.feed_actions.reset_feed_error"), - toastIDRef.current ? { id: toastIDRef.current } : undefined, - ) - }, - }) -} diff --git a/apps/renderer/src/store/feed/selector.ts b/apps/renderer/src/store/feed/selector.ts index bcfa098c8..89a381095 100644 --- a/apps/renderer/src/store/feed/selector.ts +++ b/apps/renderer/src/store/feed/selector.ts @@ -1,4 +1,13 @@ -import type { FeedModel } from "@follow/models" +import type { FeedModel, FeedOrListRespModel } from "@follow/models" + +import type { useInboxStore } from "../inbox" +import type { useListStore } from "../list" +import type { useFeedStore } from "./store" +import type { FeedQueryParams } from "./types" + +type FeedState = ReturnType +type ListState = ReturnType +type InboxState = ReturnType export const feedIconSelector = (feed: FeedModel) => { return { @@ -11,3 +20,38 @@ export const feedIconSelector = (feed: FeedModel) => { siteUrl: feed.siteUrl, } } + +export const feedByIdSelector = (feedId: Nullable) => (state: FeedState) => + feedId ? state.feeds[feedId] : null + +export const feedByIdWithTransformSelector = + (feedId: Nullable, transform: (feed: FeedModel) => T) => + (state: FeedState) => { + const feed = feedId ? state.feeds[feedId] : null + return transform ? feed && transform(feed) : feed + } + +export const feedByIdOrUrlSelector = (feed: FeedQueryParams) => (state: FeedState) => { + if (feed.id) { + return state.feeds[feed.id] + } + if (feed.url) { + return Object.values(state.feeds).find((f) => f.type === "feed" && f.url === feed.url) || null + } + return null +} + +export const feedByIdSelectorWithTransform = + (feedId: Nullable, selector: (feed: FeedOrListRespModel) => T) => + (state: FeedState) => + feedId && state.feeds[feedId] ? selector(state.feeds[feedId]) : null + +export const listByIdSelectorWithTransform = + (listId: Nullable, selector: (list: any) => T) => + (state: ListState) => + listId && state.lists[listId] ? selector(state.lists[listId]) : null + +export const inboxByIdSelectorWithTransform = + (inboxId: Nullable, selector: (inbox: any) => T) => + (state: InboxState) => + inboxId && state.inboxes[inboxId] ? selector(state.inboxes[inboxId]) : null diff --git a/apps/renderer/src/store/inbox/store.ts b/apps/renderer/src/store/inbox/store.ts index 8499d2e60..4344eb652 100644 --- a/apps/renderer/src/store/inbox/store.ts +++ b/apps/renderer/src/store/inbox/store.ts @@ -4,7 +4,7 @@ import { runTransactionInScope } from "~/database" import { apiClient } from "~/lib/api-fetch" import { InboxService } from "~/services/inbox" -import { createImmerSetter, createZustandStore } from "../utils/helper" +import { createImmerSetter, createTransaction, createZustandStore } from "../utils/helper" import type { InboxState } from "./types" export const useInboxStore = createZustandStore("inbox")(() => ({ @@ -12,7 +12,7 @@ export const useInboxStore = createZustandStore("inbox")(() => ({ })) const set = createImmerSetter(useInboxStore) - +const get = useInboxStore.getState class InboxActionStatic { upsertMany(inboxes: InboxModel[]) { if (inboxes.length === 0) return @@ -50,15 +50,20 @@ class InboxActionStatic { } async deleteInbox(inboxId: string) { - // TODO rollback - this.clearByInboxId(inboxId) - runTransactionInScope(() => InboxService.bulkDelete([inboxId])) - - await apiClient.inboxes.$delete({ - json: { - handle: inboxId, - }, + const inbox = get().inboxes[inboxId] + const tx = createTransaction(inbox) + tx.execute(async () => { + await apiClient.inboxes.$delete({ + json: { + handle: inboxId, + }, + }) }) + + tx.optimistic(async () => this.clearByInboxId(inboxId)) + tx.persist(() => InboxService.bulkDelete([inboxId])) + tx.rollback(async (inbox) => this.upsertMany([inbox])) + await tx.run() } async fetchOwnedInboxes() { diff --git a/apps/renderer/src/store/list/hooks.ts b/apps/renderer/src/store/list/hooks.ts index e95c9739c..4bb5ded10 100644 --- a/apps/renderer/src/store/list/hooks.ts +++ b/apps/renderer/src/store/list/hooks.ts @@ -1,6 +1,6 @@ import type { FeedViewType } from "@follow/constants" import type { ListModel } from "@follow/models/types" -import { useMemo } from "react" +import { useCallback, useMemo } from "react" import { useWhoami } from "~/atoms/user" @@ -10,7 +10,9 @@ export const useListById = (listId: Nullable): ListModel | null => useListStore((state) => (listId ? state.lists[listId] : null)) export const useListByView = (view: FeedViewType) => { - return useListStore((state) => Object.values(state.lists).filter((list) => list.view === view)) + return useListStore( + useCallback((state) => Object.values(state.lists).filter((list) => list.view === view), [view]), + ) } export const useOwnedListByView = (view: FeedViewType) => { @@ -24,7 +26,10 @@ export const useOwnedListByView = (view: FeedViewType) => { export const useOwnedLists = () => { const whoami = useWhoami() - return useListStore((state) => - Object.values(state.lists).filter((list) => list.ownerUserId === whoami?.id), + return useListStore( + useCallback( + (state) => Object.values(state.lists).filter((list) => list.ownerUserId === whoami?.id), + [whoami?.id], + ), ) } diff --git a/apps/renderer/src/store/subscription/getters.ts b/apps/renderer/src/store/subscription/getters.ts new file mode 100644 index 000000000..d8bd6956e --- /dev/null +++ b/apps/renderer/src/store/subscription/getters.ts @@ -0,0 +1,20 @@ +import { ROUTE_FEED_IN_LIST } from "~/constants" + +import { subscriptionCategoryExistSelector } from "./selector" +import { useSubscriptionStore } from "./store" + +const get = useSubscriptionStore.getState +export const getSubscriptionByFeedId = (feedId: FeedId) => { + const state = get() + return state.data[feedId] +} + +export const isListSubscription = (feedId?: FeedId) => { + if (!feedId) return false + const subscription = getSubscriptionByFeedId(feedId.replace(ROUTE_FEED_IN_LIST, "")) + if (!subscription) return false + return "listId" in subscription && !!subscription.listId +} + +export const subscriptionCategoryExist = (name: string) => + subscriptionCategoryExistSelector(name)(get()) diff --git a/apps/renderer/src/store/subscription/hooks.ts b/apps/renderer/src/store/subscription/hooks.ts index 3b6babc88..9254d52a3 100644 --- a/apps/renderer/src/store/subscription/hooks.ts +++ b/apps/renderer/src/store/subscription/hooks.ts @@ -1,70 +1,75 @@ -import { FeedViewType } from "@follow/constants" +import type { FeedViewType } from "@follow/constants" +import { useCallback } from "react" -import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER } from "~/constants" - -import { subscriptionCategoryExistSelector, useSubscriptionStore } from "../subscription" +import { useFeedStore } from "../feed" +import { + categoryOpenStateByViewSelector, + feedIdByViewSelector, + feedSubscriptionCountSelector, + folderFeedsByFeedIdSelector, + inboxSubscriptionCountSelector, + listSubscriptionCountSelector, + subscriptionByFeedIdSelector, + subscriptionByViewSelector, + subscriptionCategoryExistSelector, +} from "./selector" +import { useSubscriptionStore } from "./store" type FeedId = string + export const useFeedIdByView = (view: FeedViewType) => - useSubscriptionStore((state) => state.feedIdByView[view] || []) + useSubscriptionStore(useCallback((state) => feedIdByViewSelector(view)(state), [view])) export const useCategoryOpenStateByView = (view: FeedViewType) => - useSubscriptionStore((state) => state.categoryOpenStateByView[view]) + useSubscriptionStore(useCallback((state) => categoryOpenStateByViewSelector(view)(state), [view])) export const useSubscriptionByView = (view: FeedViewType) => - useSubscriptionStore((state) => state.feedIdByView[view].map((id) => state.data[id])) + useSubscriptionStore(useCallback((state) => subscriptionByViewSelector(view)(state), [view])) export const useSubscriptionByFeedId = (feedId: FeedId) => - useSubscriptionStore((state) => state.data[feedId]) + useSubscriptionStore( + useCallback((state) => subscriptionByFeedIdSelector(feedId)(state), [feedId]), + ) export const useFolderFeedsByFeedId = ({ feedId, view }: { feedId?: string; view: FeedViewType }) => - useSubscriptionStore((state): string[] | null => { - if (typeof feedId !== "string") return null - if (feedId === FEED_COLLECTION_LIST) { - return [feedId] - } - - if (!feedId.startsWith(ROUTE_FEED_IN_FOLDER)) { - return null - } - - const folderName = feedId.replace(ROUTE_FEED_IN_FOLDER, "") - const feedIds: string[] = [] - for (const feedId in state.data) { - const subscription = state.data[feedId] - if ( - subscription.view === view && - (subscription.category === folderName || subscription.defaultCategory === folderName) - ) { - feedIds.push(feedId) - } - } - return feedIds - }) - -export const useListSubscriptionCount = () => useSubscriptionStore( - (state) => - Object.values(state.data).filter((s) => !!s.listId && state.subscriptionIdSet.has(s.listId)) - .length, + useCallback((state) => folderFeedsByFeedIdSelector({ feedId, view })(state), [feedId, view]), ) -export const useInboxSubscriptionCount = () => - useSubscriptionStore( - (state) => - Object.values(state.data).filter( - (s) => !!s.inboxId && state.feedIdByView[FeedViewType.Articles].includes(s.inboxId), - ).length, - ) +export const useListSubscriptionCount = () => useSubscriptionStore(listSubscriptionCountSelector) -export const useFeedSubscriptionCount = () => - useSubscriptionStore( - (state) => - Object.values(state.data).filter( - // FIXME: Backend data compatibility - (s) => !!s.feedId && !s.listId && !s.inboxId && state.subscriptionIdSet.has(s.feedId), - ).length, - ) +export const useInboxSubscriptionCount = () => useSubscriptionStore(inboxSubscriptionCountSelector) + +export const useFeedSubscriptionCount = () => useSubscriptionStore(feedSubscriptionCountSelector) export const useSubscriptionCategoryExist = (name: string) => - useSubscriptionStore(subscriptionCategoryExistSelector(name)) + useSubscriptionStore( + useCallback((state) => subscriptionCategoryExistSelector(name)(state), [name]), + ) + +export const useAllFeeds = () => { + const feedTitleMap = useFeedStore( + useCallback((store) => { + return Object.fromEntries(Object.entries(store.feeds).map(([id, feed]) => [id, feed.title])) + }, []), + ) + return useSubscriptionStore( + useCallback( + (store) => { + const feedInfo = [] as { title: string; id: string }[] + + const allSubscriptions = Object.values(store.feedIdByView).flat() + + for (const feedId of allSubscriptions) { + const subscription = store.data[feedId] + const feed = feedTitleMap[feedId] + if (feed) { + feedInfo.push({ title: subscription.title || feed || "", id: feedId }) + } + } + return feedInfo + }, + [feedTitleMap], + ), + ) +} diff --git a/apps/renderer/src/store/subscription/index.ts b/apps/renderer/src/store/subscription/index.ts index 6a6c423c6..b7d39ce1e 100644 --- a/apps/renderer/src/store/subscription/index.ts +++ b/apps/renderer/src/store/subscription/index.ts @@ -1,3 +1,4 @@ +export * from "./getters" export * from "./hooks" export * from "./selector" export * from "./store" diff --git a/apps/renderer/src/store/subscription/selector.ts b/apps/renderer/src/store/subscription/selector.ts index a0fbadbf2..6a19f6482 100644 --- a/apps/renderer/src/store/subscription/selector.ts +++ b/apps/renderer/src/store/subscription/selector.ts @@ -1,5 +1,62 @@ +import { FeedViewType } from "@follow/constants" + +import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER } from "~/constants" + import type { useSubscriptionStore } from "./store" type State = ReturnType + export const subscriptionCategoryExistSelector = (name: string) => (state: State) => state.categories.has(name) + +export const feedSubscriptionCountSelector = (state: State) => + Object.values(state.data).filter( + // FIXME: Backend data compatibility + (s) => !!s.feedId && !s.listId && !s.inboxId && state.subscriptionIdSet.has(s.feedId), + ).length + +export const feedIdByViewSelector = (view: FeedViewType) => (state: State) => + state.feedIdByView[view] + +export const categoryOpenStateByViewSelector = (view: FeedViewType) => (state: State) => + state.categoryOpenStateByView[view] + +export const subscriptionByViewSelector = (view: FeedViewType) => (state: State) => + state.feedIdByView[view].map((id) => state.data[id]) + +export const subscriptionByFeedIdSelector = (feedId: string) => (state: State) => state.data[feedId] + +export const folderFeedsByFeedIdSelector = + ({ feedId, view }: { feedId?: string; view: FeedViewType }) => + (state: State): string[] | null => { + if (typeof feedId !== "string") return null + if (feedId === FEED_COLLECTION_LIST) { + return [feedId] + } + + if (!feedId.startsWith(ROUTE_FEED_IN_FOLDER)) { + return null + } + + const folderName = feedId.replace(ROUTE_FEED_IN_FOLDER, "") + const feedIds: string[] = [] + for (const feedId in state.data) { + const subscription = state.data[feedId] + if ( + subscription.view === view && + (subscription.category === folderName || subscription.defaultCategory === folderName) + ) { + feedIds.push(feedId) + } + } + return feedIds + } + +export const listSubscriptionCountSelector = (state: State) => + Object.values(state.data).filter((s) => !!s.listId && state.subscriptionIdSet.has(s.listId)) + .length + +export const inboxSubscriptionCountSelector = (state: State) => + Object.values(state.data).filter( + (s) => !!s.inboxId && state.feedIdByView[FeedViewType.Articles].includes(s.inboxId), + ).length diff --git a/apps/renderer/src/store/subscription/store.ts b/apps/renderer/src/store/subscription/store.ts index 7514fc49b..4b2f34a5b 100644 --- a/apps/renderer/src/store/subscription/store.ts +++ b/apps/renderer/src/store/subscription/store.ts @@ -11,7 +11,6 @@ import { omit } from "lodash-es" import { parse } from "tldts" import { whoami } from "~/atoms/user" -import { ROUTE_FEED_IN_LIST } from "~/constants" import { runTransactionInScope } from "~/database" import { apiClient } from "~/lib/api-fetch" import { queryClient } from "~/lib/query-client" @@ -24,7 +23,6 @@ import { inboxActions } from "../inbox" import { listActions } from "../list" import { feedUnreadActions } from "../unread" import { createImmerSetter, createTransaction, createZustandStore } from "../utils/helper" -import { subscriptionCategoryExistSelector } from "./selector" export type SubscriptionFlatModel = Omit & { defaultCategory?: string @@ -689,18 +687,3 @@ class SubscriptionActions { } export const subscriptionActions = new SubscriptionActions() - -export const getSubscriptionByFeedId = (feedId: FeedId) => { - const state = get() - return state.data[feedId] -} - -export const isListSubscription = (feedId?: FeedId) => { - if (!feedId) return false - const subscription = getSubscriptionByFeedId(feedId.replace(ROUTE_FEED_IN_LIST, "")) - if (!subscription) return false - return "listId" in subscription && !!subscription.listId -} - -export const subscriptionCategoryExist = (name: string) => - subscriptionCategoryExistSelector(name)(get())