feat: setting feed management table
- Updated the ModalInternal component to center the icon within the modal. - Refactored useFeedActions to utilize new modal hooks for confirming unsubscribe actions and category creation. - Introduced ConfirmDestroyModalContent and ShortcutModalContent components for better modal management. - Improved the handling of batch unsubscribe actions in the SettingFeeds component. - Updated translations for better clarity in user prompts. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
de00a1864e
commit
dbd43ae5ab
|
|
@ -366,7 +366,7 @@ export const ModalInternal = memo(function Modal({
|
|||
onPointerDownCapture={handleDrag}
|
||||
onPointerDown={relocateModal}
|
||||
>
|
||||
{!!icon && <span className="size-4">{icon}</span>}
|
||||
{!!icon && <span className="center flex size-4">{icon}</span>}
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate">
|
||||
<span>{title}</span>
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Button } from "@follow/components/ui/button/index.js"
|
||||
import type { FeedViewType } from "@follow/constants"
|
||||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { env } from "@follow/shared/env.desktop"
|
||||
|
|
@ -22,10 +21,9 @@ import { useCommandShortcuts } from "~/modules/command/hooks/use-command-binding
|
|||
import { FeedForm } from "~/modules/discover/FeedForm"
|
||||
import { InboxForm } from "~/modules/discover/InboxForm"
|
||||
import { ListForm } from "~/modules/discover/ListForm"
|
||||
import {
|
||||
CategoryCreationModalContent,
|
||||
ListCreationModalContent,
|
||||
} from "~/modules/settings/tabs/lists/modals"
|
||||
import { useConfirmUnsubscribeSubscriptionModal } from "~/modules/modal/hooks/useConfirmUnsubscribeSubscriptionModal"
|
||||
import { useCategoryCreationModal } from "~/modules/settings/tabs/lists/hooks"
|
||||
import { ListCreationModalContent } from "~/modules/settings/tabs/lists/modals"
|
||||
import { useResetFeed } from "~/queries/feed"
|
||||
import { getFeedById, useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
|
|
@ -41,24 +39,6 @@ import { useNavigateEntry } from "./useNavigateEntry"
|
|||
import { getRouteParams } from "./useRouteParams"
|
||||
import { useBatchUpdateSubscription, useDeleteSubscription } from "./useSubscriptionActions"
|
||||
|
||||
const ConfirmDestroyModalContent = ({ onConfirm }: { onConfirm: () => void }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="w-[540px]">
|
||||
<div className="mb-4">
|
||||
<i className="i-mingcute-warning-fill text-red -mb-1 mr-1 size-5" />
|
||||
{t("sidebar.feed_actions.unfollow_feed_many_warning")}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button buttonClassName="bg-red" onClick={onConfirm}>
|
||||
{t("words.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const useFeedActions = ({
|
||||
feedId,
|
||||
feedIds,
|
||||
|
|
@ -89,6 +69,7 @@ export const useFeedActions = ({
|
|||
useMemo(() => feedIds || [feedId], [feedId, feedIds]),
|
||||
)
|
||||
const { present } = useModalStack()
|
||||
const presentDeleteSubscription = useConfirmUnsubscribeSubscriptionModal()
|
||||
const deleteSubscription = useDeleteSubscription({})
|
||||
const claimFeed = useFeedClaimModal()
|
||||
|
||||
|
|
@ -99,6 +80,7 @@ export const useFeedActions = ({
|
|||
const { mutateAsync: removeFeedFromListMutation } = useRemoveFeedFromFeedList()
|
||||
const { mutateAsync: resetFeed } = useResetFeed()
|
||||
const { mutate: addFeedsToCategoryMutation } = useBatchUpdateSubscription()
|
||||
const presentCategoryCreationModal = useCategoryCreationModal()
|
||||
const openBoostModal = useBoostModal()
|
||||
|
||||
const listByView = useOwnedListByView(view!)
|
||||
|
|
@ -239,20 +221,7 @@ export const useFeedActions = ({
|
|||
label: t("sidebar.feed_column.context_menu.create_category"),
|
||||
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!,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
presentCategoryCreationModal(view!, isMultipleSelection ? feedIds : [feedId])
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
@ -281,17 +250,7 @@ export const useFeedActions = ({
|
|||
supportMultipleSelection: true,
|
||||
click: () => {
|
||||
if (isMultipleSelection) {
|
||||
present({
|
||||
title: t("sidebar.feed_actions.unfollow_feed_many_confirm"),
|
||||
content: ({ dismiss }) => (
|
||||
<ConfirmDestroyModalContent
|
||||
onConfirm={() => {
|
||||
deleteSubscription.mutate({ feedIdList: feedIds })
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
presentDeleteSubscription(feedIds)
|
||||
return
|
||||
}
|
||||
deleteSubscription.mutate({ subscription })
|
||||
|
|
@ -367,30 +326,32 @@ export const useFeedActions = ({
|
|||
item.supportMultipleSelection),
|
||||
)
|
||||
}, [
|
||||
addFeedToListMutation,
|
||||
addFeedsToCategoryMutation,
|
||||
categories,
|
||||
claimFeed,
|
||||
deleteSubscription,
|
||||
feed,
|
||||
feedId,
|
||||
feedIds,
|
||||
inbox,
|
||||
t,
|
||||
shortcuts,
|
||||
isEntryList,
|
||||
isInMASReview,
|
||||
isInbox,
|
||||
listByView,
|
||||
categories,
|
||||
isMultipleSelection,
|
||||
feedId,
|
||||
feedIds,
|
||||
claimFeed,
|
||||
resetFeed,
|
||||
openBoostModal,
|
||||
addFeedToListMutation,
|
||||
removeFeedFromListMutation,
|
||||
present,
|
||||
subscriptions,
|
||||
subscription,
|
||||
addFeedsToCategoryMutation,
|
||||
view,
|
||||
deleteSubscription,
|
||||
listByView,
|
||||
navigateEntry,
|
||||
openBoostModal,
|
||||
present,
|
||||
presentCategoryCreationModal,
|
||||
presentDeleteSubscription,
|
||||
removeFeedFromListMutation,
|
||||
resetFeed,
|
||||
shortcuts,
|
||||
subscription,
|
||||
subscriptions,
|
||||
t,
|
||||
view,
|
||||
])
|
||||
|
||||
return items
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { EventBus } from "@follow/utils/event-bus"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useShortcutsModal } from "~/modules/modal/shortcuts"
|
||||
import { useShortcutsModal } from "~/modules/modal/hooks/useShortcutsModal"
|
||||
|
||||
import { useRegisterCommandEffect } from "../hooks/use-register-command"
|
||||
import type { Command, CommandCategory } from "../types"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { Button } from "@follow/components/ui/button/index.js"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const ConfirmDestroyModalContent = ({ onConfirm }: { onConfirm: () => void }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="w-[540px]">
|
||||
<div className="mb-4 text-sm">{t("sidebar.feed_actions.unfollow_feed_many_warning")}</div>
|
||||
<div className="flex justify-end">
|
||||
<Button buttonClassName="bg-red" onClick={onConfirm}>
|
||||
{t("words.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,16 +2,14 @@ import { MotionButtonBase } from "@follow/components/ui/button/index.js"
|
|||
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
|
||||
import { clsx } from "@follow/utils/utils"
|
||||
import { m, useDragControls } from "motion/react"
|
||||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"
|
||||
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useCurrentModal } from "~/components/ui/modal/stacked/hooks"
|
||||
|
||||
import { ShortcutsGuideline } from "../command/shortcuts/SettingShortcuts"
|
||||
|
||||
const ShortcutModalContent = () => {
|
||||
export const ShortcutModalContent = () => {
|
||||
const { dismiss } = useCurrentModal()
|
||||
const modalOverlay = useUISettingKey("modalOverlay")
|
||||
const dragControls = useDragControls()
|
||||
|
|
@ -54,28 +52,3 @@ const ShortcutModalContent = () => {
|
|||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
export const useShortcutsModal = () => {
|
||||
const { present, dismiss, getModalStackById } = useModalStack()
|
||||
const id = "shortcuts"
|
||||
|
||||
const showShortcutsModal = useCallback(() => {
|
||||
present({
|
||||
title: "Shortcuts",
|
||||
id,
|
||||
overlay: false,
|
||||
content: () => <ShortcutModalContent />,
|
||||
CustomModalComponent: PlainModal,
|
||||
clickOutsideToDismiss: true,
|
||||
})
|
||||
}, [present])
|
||||
|
||||
return useCallback(() => {
|
||||
const shortcutsModal = getModalStackById(id)
|
||||
if (shortcutsModal && shortcutsModal.modal) {
|
||||
dismiss(id)
|
||||
return
|
||||
}
|
||||
showShortcutsModal()
|
||||
}, [dismiss, getModalStackById, showShortcutsModal])
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useDeleteSubscription } from "~/hooks/biz/useSubscriptionActions"
|
||||
|
||||
import { ConfirmDestroyModalContent } from "../ConfirmDestroyModalContent"
|
||||
|
||||
export const useConfirmUnsubscribeSubscriptionModal = () => {
|
||||
const { present } = useModalStack()
|
||||
const deleteSubscription = useDeleteSubscription({})
|
||||
const { t } = useTranslation()
|
||||
return useCallback(
|
||||
(feedIds: string[]) => {
|
||||
present({
|
||||
title: t("sidebar.feed_actions.unfollow_feed_many_confirm"),
|
||||
icon: <i className="i-mingcute-warning-fill text-red" />,
|
||||
content: ({ dismiss }) => (
|
||||
<ConfirmDestroyModalContent
|
||||
onConfirm={() => {
|
||||
deleteSubscription.mutate({ feedIdList: feedIds })
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
},
|
||||
[deleteSubscription, present, t],
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useCallback } from "react"
|
||||
|
||||
import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
|
||||
import { ShortcutModalContent } from "../ShortcutModalContent"
|
||||
|
||||
export const useShortcutsModal = () => {
|
||||
const { present, dismiss, getModalStackById } = useModalStack()
|
||||
const id = "shortcuts"
|
||||
|
||||
const showShortcutsModal = useCallback(() => {
|
||||
present({
|
||||
title: "Shortcuts",
|
||||
id,
|
||||
overlay: false,
|
||||
content: () => <ShortcutModalContent />,
|
||||
CustomModalComponent: PlainModal,
|
||||
clickOutsideToDismiss: true,
|
||||
})
|
||||
}, [present])
|
||||
|
||||
return useCallback(() => {
|
||||
const shortcutsModal = getModalStackById(id)
|
||||
if (shortcutsModal && shortcutsModal.modal) {
|
||||
dismiss(id)
|
||||
return
|
||||
}
|
||||
showShortcutsModal()
|
||||
}, [dismiss, getModalStackById, showShortcutsModal])
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import { Spring } from "@follow/components/constants/spring.js"
|
||||
import { MotionButtonBase } from "@follow/components/ui/button/index.js"
|
||||
import { Checkbox } from "@follow/components/ui/checkbox/index.js"
|
||||
import { Divider } from "@follow/components/ui/divider/index.js"
|
||||
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
|
||||
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
|
||||
|
|
@ -10,27 +13,296 @@ import {
|
|||
TableRow,
|
||||
} from "@follow/components/ui/table/index.jsx"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
|
||||
import { views } from "@follow/constants"
|
||||
import clsx from "clsx"
|
||||
import { AnimatePresence, m } from "motion/react"
|
||||
import type { FC } from "react"
|
||||
import { memo, useCallback, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useIsInMASReview } from "~/atoms/server-configs"
|
||||
import { RelativeDay } from "~/components/ui/datetime"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu/dropdown-menu"
|
||||
import { useDialog } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useBatchUpdateSubscription } from "~/hooks/biz/useSubscriptionActions"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { UrlBuilder } from "~/lib/url-builder"
|
||||
import { FeedIcon } from "~/modules/feed/feed-icon"
|
||||
import { useConfirmUnsubscribeSubscriptionModal } from "~/modules/modal/hooks/useConfirmUnsubscribeSubscriptionModal"
|
||||
import { Balance } from "~/modules/wallet/balance"
|
||||
import { Queries } from "~/queries"
|
||||
import { useFeedById } from "~/store/feed"
|
||||
import { useAllFeeds, useSubscriptionByFeedId } from "~/store/subscription"
|
||||
|
||||
export const SettingFeeds = () => {
|
||||
const inMas = useIsInMASReview()
|
||||
return (
|
||||
<div className="space-y-4 pb-8">
|
||||
<SubscriptionFeedsSection />
|
||||
{!inMas && <FeedClaimedSection />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const GRID_COLS_CLASSNAME = tw`grid-cols-[30px_auto_150px_100px]`
|
||||
|
||||
const SubscriptionFeedsSection = () => {
|
||||
const { t } = useTranslation("settings")
|
||||
const feedList = useAllFeeds()
|
||||
const [selectedFeeds, setSelectedFeeds] = useState<Set<string>>(() => new Set())
|
||||
|
||||
const handleSelectAll = useCallback(
|
||||
(checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFeeds(new Set(feedList.map((feed) => feed.id)))
|
||||
} else {
|
||||
setSelectedFeeds(new Set())
|
||||
}
|
||||
},
|
||||
[feedList],
|
||||
)
|
||||
|
||||
const handleSelectFeed = useCallback((feedId: string, checked: boolean) => {
|
||||
setSelectedFeeds((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (checked) {
|
||||
newSet.add(feedId)
|
||||
} else {
|
||||
newSet.delete(feedId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}, [])
|
||||
|
||||
const isAllSelected = feedList.length > 0 && selectedFeeds.size === feedList.length
|
||||
|
||||
const presentDeleteSubscription = useConfirmUnsubscribeSubscriptionModal()
|
||||
const handleBatchUnsubscribe = useCallback(() => {
|
||||
const feedIds = Array.from(selectedFeeds)
|
||||
presentDeleteSubscription(feedIds)
|
||||
}, [presentDeleteSubscription, selectedFeeds])
|
||||
|
||||
return (
|
||||
<section className="relative mt-4">
|
||||
<h2 className="mb-2 text-lg font-semibold">{t("feeds.subscription")}</h2>
|
||||
|
||||
{feedList.length > 0 && (
|
||||
<div className="mt-8 space-y-1">
|
||||
{/* Header - Sticky */}
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-background text-text-secondary sticky top-0 z-20 grid h-8 gap-4 border-b px-1 pb-2 text-sm font-medium",
|
||||
GRID_COLS_CLASSNAME,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} />
|
||||
</div>
|
||||
<div>{t("feeds.tableHeaders.name")}</div>
|
||||
<div className="ml-4">View</div>
|
||||
<div className="text-center">Date</div>
|
||||
</div>
|
||||
|
||||
{/* Feed List */}
|
||||
{feedList.map((item) => {
|
||||
return (
|
||||
<FeedListItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
selected={selectedFeeds.has(item.id)}
|
||||
onSelect={handleSelectFeed}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Sticky Action Bar at bottom when scrolled */}
|
||||
<AnimatePresence>
|
||||
{selectedFeeds.size > 0 && (
|
||||
<>
|
||||
<m.div
|
||||
initial={{ opacity: 0, transform: "translateY(10px)" }}
|
||||
animate={{ opacity: 1, transform: "translateY(0)" }}
|
||||
exit={{ opacity: 0, transform: "translateY(10px)" }}
|
||||
transition={Spring.presets.smooth}
|
||||
className="sticky bottom-16 mt-4 flex justify-center"
|
||||
>
|
||||
<div className="text-text-secondary bg-material-opaque rounded-md px-4 py-2 text-sm">
|
||||
{selectedFeeds.size} item(s) selected
|
||||
<button
|
||||
className="text-accent cursor-button ml-3 text-xs"
|
||||
type="button"
|
||||
onClick={() => setSelectedFeeds(new Set())}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</m.div>
|
||||
<m.div
|
||||
initial={{ opacity: 0, transform: "translateY(10px)" }}
|
||||
animate={{ opacity: 1, transform: "translateY(0)" }}
|
||||
exit={{ opacity: 0, transform: "translateY(10px)" }}
|
||||
transition={Spring.presets.smooth}
|
||||
className="sticky bottom-4 flex justify-center"
|
||||
>
|
||||
<div className="bg-material-opaque flex items-center gap-2 rounded px-4 py-2">
|
||||
{/* <DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="text-accent text-xs"
|
||||
type="button"
|
||||
onClick={handleBatchMoveToView}
|
||||
>
|
||||
Add to Category
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent side="top">
|
||||
<CategorySelector />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu> */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<MotionButtonBase className="text-accent text-xs" type="button">
|
||||
Move to View
|
||||
</MotionButtonBase>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top">
|
||||
<ViewSelector selectedFeeds={selectedFeeds} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<MotionButtonBase
|
||||
className="text-red text-xs"
|
||||
type="button"
|
||||
onClick={handleBatchUnsubscribe}
|
||||
>
|
||||
Unsubscribe
|
||||
</MotionButtonBase>
|
||||
</div>
|
||||
</m.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const ViewSelector: FC<{ selectedFeeds: Set<string> }> = ({ selectedFeeds }) => {
|
||||
const { t: tCommon } = useTranslation("common")
|
||||
const { mutate: batchUpdateSubscription } = useBatchUpdateSubscription()
|
||||
const { ask } = useDialog()
|
||||
return views.map((view) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={view.view}
|
||||
icon={view.icon}
|
||||
onClick={() => {
|
||||
ask({
|
||||
title: "Confirm",
|
||||
message: `Are you sure you want to move these feeds to ${tCommon(view.name)}?`,
|
||||
onConfirm: () => {
|
||||
batchUpdateSubscription({
|
||||
feedIdList: Array.from(selectedFeeds),
|
||||
view: view.view,
|
||||
})
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
{tCommon(view.name)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const FeedListItem = memo(
|
||||
({
|
||||
id,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
id: string
|
||||
selected: boolean
|
||||
onSelect: (feedId: string, checked: boolean) => void
|
||||
}) => {
|
||||
const subscription = useSubscriptionByFeedId(id)
|
||||
const feed = useFeedById(id)
|
||||
const isCustomizeName = subscription?.title && feed?.title !== subscription?.title
|
||||
const { t: tCommon } = useTranslation("common")
|
||||
|
||||
if (!subscription) return null
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className={clsx(
|
||||
"hover:bg-material-medium grid h-10 w-full items-center gap-4 rounded px-1",
|
||||
GRID_COLS_CLASSNAME,
|
||||
)}
|
||||
onClick={() => onSelect(id, !selected)}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox checked={selected} onCheckedChange={(checked) => onSelect(id, !!checked)} />
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<FeedIcon feed={feed} size={16} />
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<EllipsisHorizontalTextWithTooltip className="text-text font-medium">
|
||||
{subscription.title || feed?.title}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
{isCustomizeName && (
|
||||
<EllipsisHorizontalTextWithTooltip className="text-text-secondary text-sm">
|
||||
{feed?.title}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-text-secondary text-sm">{views[subscription.view]!.icon}</span>
|
||||
<span>{tCommon(views[subscription.view]!.name)}</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<RelativeDay date={new Date(subscription.createdAt)} />
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const FeedClaimedSection = () => {
|
||||
const { t } = useTranslation("settings")
|
||||
const claimedList = useAuthQuery(Queries.feed.claimedList())
|
||||
|
||||
const numberFormatter = useMemo(() => new Intl.NumberFormat("en-US", {}), [])
|
||||
|
||||
return (
|
||||
<section className="mt-4">
|
||||
<div>
|
||||
<h2 className="mb-2 text-lg font-semibold">{t("feeds.claim")}</h2>
|
||||
</div>
|
||||
<div className="mb-4 space-y-2 text-sm">
|
||||
<p>{t("feeds.claimTips")}</p>
|
||||
</div>
|
||||
<Divider className="mb-6 mt-8" />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<ScrollArea.ScrollArea viewportClassName="max-h-[380px]">
|
||||
{claimedList.data?.length ? (
|
||||
{claimedList.isLoading ? (
|
||||
<LoadingCircle size="large" className="center h-36" />
|
||||
) : !claimedList.data?.length ? (
|
||||
<div className="text-text-secondary mt-36 w-full text-center text-sm">
|
||||
<p>{t("feeds.noFeeds")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{claimedList.data?.length ? (
|
||||
<ScrollArea.ScrollArea viewportClassName="max-h-[380px]">
|
||||
<Table className="mt-4">
|
||||
<TableHeader className="border-b">
|
||||
<TableRow className="[&_*]:!font-semibold">
|
||||
|
|
@ -61,7 +333,7 @@ export const SettingFeeds = () => {
|
|||
</a>
|
||||
</TableCell>
|
||||
<TableCell align="center" className="tabular-nums" size="sm">
|
||||
{row.subscriptionCount}
|
||||
{numberFormatter.format(row.subscriptionCount)}
|
||||
</TableCell>
|
||||
<TableCell align="center" size="sm">
|
||||
<Balance>{BigInt(row.tipAmount || 0n)}</Balance>
|
||||
|
|
@ -70,14 +342,8 @@ export const SettingFeeds = () => {
|
|||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : claimedList.isLoading ? (
|
||||
<LoadingCircle size="large" className="center absolute inset-0" />
|
||||
) : (
|
||||
<div className="mt-36 w-full text-center text-sm text-zinc-400">
|
||||
<p>{t("feeds.noFeeds")}</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea.ScrollArea>
|
||||
</ScrollArea.ScrollArea>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import type { FeedViewType } from "@follow/constants"
|
||||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useBatchUpdateSubscription } from "~/hooks/biz/useSubscriptionActions"
|
||||
|
||||
import { CategoryCreationModalContent } from "./modals"
|
||||
|
||||
export const useCategoryCreationModal = () => {
|
||||
const { t } = useTranslation()
|
||||
const { present } = useModalStack()
|
||||
const { mutate: addFeedsToCategoryMutation } = useBatchUpdateSubscription()
|
||||
return useCallback(
|
||||
(view: FeedViewType, feedIds: string[]) =>
|
||||
present({
|
||||
title: t("sidebar.feed_column.context_menu.title"),
|
||||
content: () => (
|
||||
<CategoryCreationModalContent
|
||||
onSubmit={(category: string) => {
|
||||
addFeedsToCategoryMutation({
|
||||
feedIdList: feedIds,
|
||||
category,
|
||||
view: view!,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[addFeedsToCategoryMutation, present, t],
|
||||
)
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ export const loader = defineSettingPageData({
|
|||
icon: iconName,
|
||||
name: "titles.feeds",
|
||||
priority,
|
||||
hideIf: (ctx) => ctx.isInMASReview,
|
||||
})
|
||||
|
||||
export function Component() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { FeedViewType, viewList } from "@follow/constants"
|
||||
import type { FeedViewType } from "@follow/constants"
|
||||
import { useCallback, useMemo } from "react"
|
||||
|
||||
import { useGeneralSettingSelector } from "~/atoms/settings/general"
|
||||
|
||||
import { useFeedStore } from "../feed"
|
||||
import {
|
||||
categoriesSelector,
|
||||
categoryOpenStateByViewSelector,
|
||||
feedIdByViewSelector,
|
||||
feedSubscriptionCountSelector,
|
||||
|
|
@ -15,7 +16,10 @@ import {
|
|||
subscriptionByFeedIdSelector,
|
||||
subscriptionByViewSelector,
|
||||
subscriptionCategoryExistSelector,
|
||||
subscriptionInboxSelector,
|
||||
subscriptionListSelector,
|
||||
subscriptionsByFeedIsdSelector,
|
||||
viewWithSubscriptionSelector,
|
||||
} from "./selector"
|
||||
import { useSubscriptionStore } from "./store"
|
||||
|
||||
|
|
@ -34,26 +38,9 @@ export const useSubscriptionByView = (view: FeedViewType) =>
|
|||
useCallback((state) => subscriptionByViewSelector(view)(state) || [], [view]),
|
||||
)
|
||||
|
||||
export const useViewWithSubscription = () =>
|
||||
useSubscriptionStore(
|
||||
useCallback((state) => {
|
||||
return viewList.filter((view) => {
|
||||
if (
|
||||
view === FeedViewType.Articles ||
|
||||
view === FeedViewType.SocialMedia ||
|
||||
view === FeedViewType.Pictures ||
|
||||
view === FeedViewType.Videos
|
||||
) {
|
||||
return true
|
||||
} else {
|
||||
return state.feedIdByView[view].length > 0
|
||||
}
|
||||
})
|
||||
}, []),
|
||||
)
|
||||
export const useViewWithSubscription = () => useSubscriptionStore(viewWithSubscriptionSelector)
|
||||
|
||||
export const useCategories = () =>
|
||||
useSubscriptionStore(useCallback((state) => state.categories || [], []))
|
||||
export const useCategories = () => useSubscriptionStore(categoriesSelector)
|
||||
|
||||
export const useCategoriesByView = (view: FeedViewType) =>
|
||||
useSubscriptionStore(
|
||||
|
|
@ -129,21 +116,11 @@ export const useAllFeeds = () => {
|
|||
}
|
||||
|
||||
export const useAllLists = () => {
|
||||
return useSubscriptionStore(
|
||||
useCallback(
|
||||
(store) => Object.values(store.data).filter((subscription) => subscription.listId),
|
||||
[],
|
||||
),
|
||||
)
|
||||
return useSubscriptionStore(subscriptionListSelector)
|
||||
}
|
||||
|
||||
export const useAllInboxes = () => {
|
||||
return useSubscriptionStore(
|
||||
useCallback(
|
||||
(store) => Object.values(store.data).filter((subscription) => subscription.inboxId),
|
||||
[],
|
||||
),
|
||||
)
|
||||
return useSubscriptionStore(subscriptionInboxSelector)
|
||||
}
|
||||
|
||||
export const useFeedsGroupedData = (view: FeedViewType) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { FeedViewType } from "@follow/constants"
|
||||
import { FeedViewType, viewList } from "@follow/constants"
|
||||
|
||||
import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER } from "~/constants"
|
||||
|
||||
|
|
@ -56,3 +56,26 @@ export const listSubscriptionCountSelector = (state: State) => state.listIds.siz
|
|||
export const inboxSubscriptionCountSelector = (state: State) => state.inboxIds.size
|
||||
|
||||
export const isSubscribedSelector = (feedId: string) => (state: State) => !!state.data[feedId]
|
||||
|
||||
export const subscriptionListSelector = (state: State) =>
|
||||
Object.values(state.data).filter((subscription) => subscription.listId)
|
||||
|
||||
export const subscriptionInboxSelector = (store: State) =>
|
||||
Object.values(store.data).filter((subscription) => subscription.inboxId)
|
||||
|
||||
export const viewWithSubscriptionSelector = (state: State) => {
|
||||
return viewList.filter((view) => {
|
||||
if (
|
||||
view === FeedViewType.Articles ||
|
||||
view === FeedViewType.SocialMedia ||
|
||||
view === FeedViewType.Pictures ||
|
||||
view === FeedViewType.Videos
|
||||
) {
|
||||
return true
|
||||
} else {
|
||||
return state.feedIdByView[view].length > 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const categoriesSelector = (state: State) => state.categories || []
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@
|
|||
"sidebar.feed_actions.resetting_feed": "Resetting feed...",
|
||||
"sidebar.feed_actions.unfollow": "Unfollow",
|
||||
"sidebar.feed_actions.unfollow_feed": "Unfollow Feed",
|
||||
"sidebar.feed_actions.unfollow_feed_many": "Unfollow all selected feeds",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -145,8 +145,10 @@
|
|||
"discoverFilters.filters": "Filters",
|
||||
"discoverFilters.language": "Language",
|
||||
"discoverFilters.title": "Discover Filters",
|
||||
"feeds.claim": "Claim Feeds",
|
||||
"feeds.claimTips": "To claim your feeds and receive tips, right-click on the feed in your subscription list and select Claim.",
|
||||
"feeds.noFeeds": "No claimed feeds",
|
||||
"feeds.subscription": "Subscribed Feeds",
|
||||
"feeds.tableHeaders.name": "Name",
|
||||
"feeds.tableHeaders.subscriptionCount": "Subs",
|
||||
"feeds.tableHeaders.tipAmount": "Tips",
|
||||
|
|
|
|||
Loading…
Reference in New Issue