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 <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Innei <tukon479@gmail.com>
This commit is contained in:
Lava 2024-12-09 01:03:20 -08:00 committed by GitHub
parent 84a188a8e1
commit d684e14056
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 154 additions and 3 deletions

View File

@ -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: <i className="i-mgc-add-cute-re" />,
click() {
present({
title: t("sidebar.feed_column.context_menu.title"),
content: () => (
<CategoryCreationModalContent
onSubmit={(category: string) => {
addFeedsToCategoryMutation({
feedIdList: isMultipleSelection ? feedIds : [feedId],
category,
view: view!,
})
}}
/>
),
})
},
},
],
},
{
type: "separator" as const,
disabled: isEntryList,

View File

@ -319,3 +319,56 @@ const RowRender = ({ feedId, listId }: { feedId: string; listId: string }) => {
</TableRow>
)
}
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<z.infer<typeof categoryFormSchema>>({
resolver: zodResolver(categoryFormSchema),
defaultValues: {
categoryName: "",
},
})
const handleSubmit = form.handleSubmit(({ categoryName }) => {
onSubmit(categoryName)
dismiss()
})
return (
<Form {...form}>
<form onSubmit={handleSubmit} className="space-y-4 lg:w-[450px]">
<FormField
control={form.control}
name="categoryName"
render={({ field }) => (
<FormItem>
<div>
<FormLabel>
{t("sidebar.feed_column.context_menu.new_category_modal.category_name")}
<sup className="ml-1 align-sub text-red-500">*</sup>
</FormLabel>
</div>
<FormControl>
<Input autoFocus {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">
{t("sidebar.feed_column.context_menu.new_category_modal.create")}
</Button>
</div>
</form>
</Form>
)
}

View File

@ -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(

View File

@ -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 }) =>

View File

@ -2,6 +2,8 @@
## New Features
- New `Move to Category` operation in feed subscription context menu
## Improvements
## Bug Fixes

View File

@ -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",

View File

@ -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": "登入至",