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:
parent
84a188a8e1
commit
d684e14056
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 }) =>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## New Features
|
||||
|
||||
- New `Move to Category` operation in feed subscription context menu
|
||||
|
||||
## Improvements
|
||||
|
||||
## Bug Fixes
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "登入至",
|
||||
|
|
|
|||
Loading…
Reference in New Issue