From d684e14056581744ac78ca697fa5ca059294245e Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Mon, 9 Dec 2024 01:03:20 -0800 Subject: [PATCH] feat: move feed to new category in context menu (#2072) * feat: move feed to new category in context menu * chore: update `next.md` * fix: perf Signed-off-by: Innei --------- Signed-off-by: Innei Co-authored-by: Innei --- .../renderer/src/hooks/biz/useFeedActions.tsx | 68 ++++++++++++++++++- .../modules/settings/tabs/lists/modals.tsx | 53 +++++++++++++++ apps/renderer/src/store/subscription/hooks.ts | 22 ++++++ .../src/store/subscription/selector.ts | 2 + changelog/next.md | 2 + locales/app/en.json | 5 ++ locales/app/zh-CN.json | 5 ++ 7 files changed, 154 insertions(+), 3 deletions(-) diff --git a/apps/renderer/src/hooks/biz/useFeedActions.tsx b/apps/renderer/src/hooks/biz/useFeedActions.tsx index a433689df..59a96a832 100644 --- a/apps/renderer/src/hooks/biz/useFeedActions.tsx +++ b/apps/renderer/src/hooks/biz/useFeedActions.tsx @@ -18,16 +18,24 @@ 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 { + CategoryCreationModalContent, + ListCreationModalContent, +} from "~/modules/settings/tabs/lists/modals" import { useResetFeed } from "~/queries/feed" import { getFeedById, useFeedById } from "~/store/feed" import { useInboxById } from "~/store/inbox" import { listActions, useListById, useOwnedListByView } from "~/store/list" -import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription" +import { + subscriptionActions, + useCategoriesByView, + useSubscriptionByFeedId, + useSubscriptionsByFeedIds, +} from "~/store/subscription" import { useNavigateEntry } from "./useNavigateEntry" import { getRouteParams } from "./useRouteParams" -import { useDeleteSubscription } from "./useSubscriptionActions" +import { useBatchUpdateSubscription, useDeleteSubscription } from "./useSubscriptionActions" const ConfirmDestroyModalContent = ({ onConfirm }: { onConfirm: () => void }) => { const { t } = useTranslation() @@ -68,9 +76,14 @@ export const useFeedActions = ({ siteUrl: feed.siteUrl, } }) + const inbox = useInboxById(feedId) const isInbox = !!inbox const subscription = useSubscriptionByFeedId(feedId) + + const subscriptions = useSubscriptionsByFeedIds( + useMemo(() => feedIds || [feedId], [feedId, feedIds]), + ) const { present } = useModalStack() const deleteSubscription = useDeleteSubscription({}) const claimFeed = useFeedClaimModal() @@ -81,9 +94,11 @@ export const useFeedActions = ({ const { mutateAsync: addFeedToListMutation } = useAddFeedToFeedList() const { mutateAsync: removeFeedFromListMutation } = useRemoveFeedFromFeedList() const { mutateAsync: resetFeed } = useResetFeed() + const { mutate: addFeedsToCategoryMutation } = useBatchUpdateSubscription() const openBoostModal = useBoostModal() const listByView = useOwnedListByView(view!) + const categories = useCategoriesByView(view!) const isMultipleSelection = feedIds && feedIds.length > 1 && feedIds.includes(feedId) @@ -192,6 +207,53 @@ export const useFeedActions = ({ }, ], }, + { + type: "text" as const, + label: t("sidebar.feed_column.context_menu.add_feeds_to_category"), + disabled: isInbox, + supportMultipleSelection: true, + submenu: [ + ...categories.values().map((category) => { + const isIncluded = isMultipleSelection + ? subscriptions.every((s) => s.category === category) + : subscription?.category === category + return { + label: category, + type: "text" as const, + checked: isIncluded, + click() { + addFeedsToCategoryMutation({ + feedIdList: isMultipleSelection ? feedIds : [feedId], + category: isIncluded ? null : category, // if already included, remove it + view: view!, + }) + }, + } + }), + listByView.length > 0 && { type: "separator" as const }, + { + label: t("sidebar.feed_column.context_menu.create_category"), + type: "text" as const, + icon: , + click() { + present({ + title: t("sidebar.feed_column.context_menu.title"), + content: () => ( + { + addFeedsToCategoryMutation({ + feedIdList: isMultipleSelection ? feedIds : [feedId], + category, + view: view!, + }) + }} + /> + ), + }) + }, + }, + ], + }, { type: "separator" as const, disabled: isEntryList, diff --git a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx index 4af1d1df6..fa54f6ab1 100644 --- a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx +++ b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx @@ -319,3 +319,56 @@ const RowRender = ({ feedId, listId }: { feedId: string; listId: string }) => { ) } +const categoryFormSchema = z.object({ + categoryName: z.string().min(1), +}) +export const CategoryCreationModalContent = ({ + onSubmit, +}: { + onSubmit: (category: string) => void +}) => { + const { dismiss } = useCurrentModal() + const { t } = useTranslation() + + const form = useForm>({ + resolver: zodResolver(categoryFormSchema), + defaultValues: { + categoryName: "", + }, + }) + + const handleSubmit = form.handleSubmit(({ categoryName }) => { + onSubmit(categoryName) + dismiss() + }) + + return ( +
+ + ( + +
+ + {t("sidebar.feed_column.context_menu.new_category_modal.category_name")} + * + +
+ + + + +
+ )} + /> +
+ +
+ + + ) +} diff --git a/apps/renderer/src/store/subscription/hooks.ts b/apps/renderer/src/store/subscription/hooks.ts index 1c718d1df..9d37fc101 100644 --- a/apps/renderer/src/store/subscription/hooks.ts +++ b/apps/renderer/src/store/subscription/hooks.ts @@ -12,6 +12,7 @@ import { subscriptionByFeedIdSelector, subscriptionByViewSelector, subscriptionCategoryExistSelector, + subscriptionsByFeedIsdSelector, } from "./selector" import { useSubscriptionStore } from "./store" @@ -30,10 +31,31 @@ export const useSubscriptionByView = (view: FeedViewType) => useCallback((state) => subscriptionByViewSelector(view)(state) || [], [view]), ) +export const useCategories = () => + useSubscriptionStore(useCallback((state) => state.categories || [], [])) + +export const useCategoriesByView = (view: FeedViewType) => + useSubscriptionStore( + useCallback( + (state) => + new Set( + subscriptionByViewSelector(view)(state) + .map((subscription) => subscription.category) + .filter((category) => category !== null && category !== undefined) + .filter(Boolean), + ), + [view], + ), + ) + export const useSubscriptionByFeedId = (feedId: FeedId) => useSubscriptionStore( useCallback((state) => subscriptionByFeedIdSelector(feedId)(state) || null, [feedId]), ) +export const useSubscriptionsByFeedIds = (feedIds: FeedId[]) => + useSubscriptionStore( + useCallback((state) => subscriptionsByFeedIsdSelector(feedIds)(state) || null, [feedIds]), + ) export const useFolderFeedsByFeedId = ({ feedId, view }: { feedId?: string; view: FeedViewType }) => useSubscriptionStore( diff --git a/apps/renderer/src/store/subscription/selector.ts b/apps/renderer/src/store/subscription/selector.ts index 6a19f6482..c04565d88 100644 --- a/apps/renderer/src/store/subscription/selector.ts +++ b/apps/renderer/src/store/subscription/selector.ts @@ -25,6 +25,8 @@ 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 subscriptionsByFeedIsdSelector = (feedIds: string[]) => (state: State) => + feedIds.map((id) => state.data[id]) export const folderFeedsByFeedIdSelector = ({ feedId, view }: { feedId?: string; view: FeedViewType }) => diff --git a/changelog/next.md b/changelog/next.md index bee81a9a2..7dd8188c3 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -2,6 +2,8 @@ ## New Features +- New `Move to Category` operation in feed subscription context menu + ## Improvements ## Bug Fixes diff --git a/locales/app/en.json b/locales/app/en.json index 50de42ca0..9f0f83210 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -336,14 +336,19 @@ "sidebar.feed_actions.unfollow_feed_many": "Unfollow all selected feeds", "sidebar.feed_actions.unfollow_feed_many_confirm": "Confirm unfollow all selected feeds?", "sidebar.feed_actions.unfollow_feed_many_warning": "Warning: This operation will unfollow all selected feeds, and cannot be undone.", + "sidebar.feed_column.context_menu.add_feeds_to_category": "Move to Category", "sidebar.feed_column.context_menu.add_feeds_to_list": "Add Feeds to List", "sidebar.feed_column.context_menu.change_to_other_view": "Switch to Another View", + "sidebar.feed_column.context_menu.create_category": "New Category", "sidebar.feed_column.context_menu.delete_category": "Delete Category", "sidebar.feed_column.context_menu.delete_category_confirmation": "Delete Category {{folderName}}?", "sidebar.feed_column.context_menu.mark_as_read": "Mark as Read", + "sidebar.feed_column.context_menu.new_category_modal.category_name": "Category Name", + "sidebar.feed_column.context_menu.new_category_modal.create": "Create", "sidebar.feed_column.context_menu.rename_category": "Rename Category", "sidebar.feed_column.context_menu.rename_category_error": "Failed to rename category", "sidebar.feed_column.context_menu.rename_category_success": "Category renamed successfully", + "sidebar.feed_column.context_menu.title": "Move to New Category", "sidebar.select_sort_method": "Select a sort method", "signin.continue_with": "Continue with {{provider}}", "signin.sign_in_to": "Sign in to", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index c3c99c37d..5008f97de 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -335,14 +335,19 @@ "sidebar.feed_actions.unfollow_feed_many": "取消订阅选中的订阅源", "sidebar.feed_actions.unfollow_feed_many_confirm": "确认要取消订阅选中的订阅源?", "sidebar.feed_actions.unfollow_feed_many_warning": "警告:这个操作将取消订阅所有选中的订阅源,并且不可恢复。", + "sidebar.feed_column.context_menu.add_feeds_to_category": "移动至分类", "sidebar.feed_column.context_menu.add_feeds_to_list": "添加订阅源到列表", "sidebar.feed_column.context_menu.change_to_other_view": "更改为其他视图", + "sidebar.feed_column.context_menu.create_category": "新分类", "sidebar.feed_column.context_menu.delete_category": "删除分类", "sidebar.feed_column.context_menu.delete_category_confirmation": "删除分类 {{folderName}}?", "sidebar.feed_column.context_menu.mark_as_read": "标记为已读", + "sidebar.feed_column.context_menu.new_category_modal.category_name": "分类名称", + "sidebar.feed_column.context_menu.new_category_modal.create": "创建", "sidebar.feed_column.context_menu.rename_category": "重命名分类", "sidebar.feed_column.context_menu.rename_category_error": "重命名分类失败", "sidebar.feed_column.context_menu.rename_category_success": "分类重命名成功", + "sidebar.feed_column.context_menu.title": "移动至新分类", "sidebar.select_sort_method": "选择排序方法", "signin.continue_with": "使用 {{provider}} 登入", "signin.sign_in_to": "登入至",