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:
Innei 2025-05-28 00:12:45 +08:00
parent de00a1864e
commit dbd43ae5ab
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
14 changed files with 453 additions and 142 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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],
)
}

View File

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

View File

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

View File

@ -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],
)
}

View File

@ -9,7 +9,6 @@ export const loader = defineSettingPageData({
icon: iconName,
name: "titles.feeds",
priority,
hideIf: (ctx) => ctx.isInMASReview,
})
export function Component() {

View File

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

View File

@ -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 || []

View File

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

View File

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