mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Add Infrequent tab and corresponding user setting
This commit is contained in:
@@ -18,6 +18,13 @@ const categories: Record<string, Omit<Category, "name">> = {
|
||||
feeds: [],
|
||||
position: 1,
|
||||
},
|
||||
infrequent: {
|
||||
id: "infrequent",
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
position: 2,
|
||||
},
|
||||
}
|
||||
|
||||
const sharing: {
|
||||
@@ -105,6 +112,7 @@ export const Constants = {
|
||||
tooltip: {
|
||||
delay: 500,
|
||||
},
|
||||
infrequentThresholdDaysDefault: 7,
|
||||
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
||||
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
|
||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Subscription {
|
||||
filterLegacy?: string
|
||||
pushNotificationsEnabled: boolean
|
||||
autoMarkAsReadAfterDays?: number
|
||||
averageEntryIntervalMs?: number
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
@@ -284,6 +285,7 @@ export interface Settings {
|
||||
unreadCountTitle: boolean
|
||||
unreadCountFavicon: boolean
|
||||
disablePullToRefresh: boolean
|
||||
infrequentThresholdDays: number
|
||||
primaryColor?: string
|
||||
sharingSettings: SharingSettings
|
||||
pushNotificationSettings: PushNotificationSettings
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
changeDisablePullToRefresh,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeInfrequentThresholdDays,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMarkAllAsReadNavigateToUnread,
|
||||
@@ -141,6 +142,10 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.disablePullToRefresh = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeInfrequentThresholdDays.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.infrequentThresholdDays = action.meta.arg
|
||||
})
|
||||
builder.addCase(changePrimaryColor.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.primaryColor = action.meta.arg
|
||||
@@ -171,6 +176,7 @@ export const userSlice = createSlice({
|
||||
changeUnreadCountTitle.fulfilled,
|
||||
changeUnreadCountFavicon.fulfilled,
|
||||
changeDisablePullToRefresh.fulfilled,
|
||||
changeInfrequentThresholdDays.fulfilled,
|
||||
changePrimaryColor.fulfilled,
|
||||
changeSharingSetting.fulfilled,
|
||||
changePushNotificationSettings.fulfilled
|
||||
|
||||
@@ -158,6 +158,15 @@ export const changeSharingSetting = createAppAsyncThunk(
|
||||
}
|
||||
)
|
||||
|
||||
export const changeInfrequentThresholdDays = createAppAsyncThunk(
|
||||
"settings/infrequentThresholdDays",
|
||||
(infrequentThresholdDays: number, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, infrequentThresholdDays })
|
||||
}
|
||||
)
|
||||
|
||||
export const changePushNotificationSettings = createAppAsyncThunk(
|
||||
"settings/pushNotificationSettings",
|
||||
(pushNotificationSettings: PushNotificationSettings, thunkApi) => {
|
||||
|
||||
@@ -26,20 +26,22 @@ export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
|
||||
return categories
|
||||
}
|
||||
|
||||
export function categoryUnreadCount(category?: TreeCategory): number {
|
||||
export function categoryUnreadCount(category?: TreeCategory, maxFrequencyThresholdMs?: number): number {
|
||||
if (!category) return 0
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
.flatMap(c => c.feeds)
|
||||
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
|
||||
.map(f => f.unread)
|
||||
.reduce((total, current) => total + current, 0)
|
||||
}
|
||||
|
||||
export function categoryHasNewEntries(category?: TreeCategory): boolean {
|
||||
export function categoryHasNewEntries(category?: TreeCategory, maxFrequencyThresholdMs?: number): boolean {
|
||||
if (!category) return false
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
.flatMap(c => c.feeds)
|
||||
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
|
||||
.some(f => f.hasNewEntries)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
changeDisablePullToRefresh,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeInfrequentThresholdDays,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMarkAllAsReadNavigateToUnread,
|
||||
@@ -44,6 +45,7 @@ export function DisplaySettings() {
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const disablePullToRefresh = useAppSelector(state => state.user.settings?.disablePullToRefresh)
|
||||
const infrequentThresholdDays = useAppSelector(state => state.user.settings?.infrequentThresholdDays)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
|
||||
const { _ } = useLingui()
|
||||
@@ -143,6 +145,14 @@ export function DisplaySettings() {
|
||||
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label={<Trans>Infrequent posts threshold (days)</Trans>}
|
||||
description={<Trans>Feeds posting less often than this (on average) will appear in the Infrequent view</Trans>}
|
||||
min={1}
|
||||
value={infrequentThresholdDays}
|
||||
onChange={async value => await dispatch(changeInfrequentThresholdDays(+value))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
|
||||
|
||||
<Switch
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Stack } from "@mantine/core"
|
||||
import React from "react"
|
||||
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||
import { TbChevronDown, TbChevronRight, TbClock, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||
import { Constants } from "@/app/constants"
|
||||
import {
|
||||
redirectToCategory,
|
||||
@@ -23,6 +23,7 @@ import { TreeSearch } from "./TreeSearch"
|
||||
|
||||
const allIcon = <TbInbox size={16} />
|
||||
const starredIcon = <TbStar size={16} />
|
||||
const infrequentIcon = <TbClock size={16} />
|
||||
const tagIcon = <TbTag size={16} />
|
||||
const expandedIcon = <TbChevronDown size={16} />
|
||||
const collapsedIcon = <TbChevronRight size={16} />
|
||||
@@ -34,6 +35,10 @@ export function Tree() {
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const infrequentThresholdDays = useAppSelector(
|
||||
state => state.user.settings?.infrequentThresholdDays ?? Constants.infrequentThresholdDaysDefault
|
||||
)
|
||||
const infrequentThresholdMs = infrequentThresholdDays * 24 * 3600 * 1000
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isFeedDisplayed = (feed: Subscription) => {
|
||||
@@ -115,6 +120,22 @@ export function Tree() {
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
const infrequentCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.infrequent.id}
|
||||
type="category"
|
||||
name={<Trans>Infrequent</Trans>}
|
||||
icon={infrequentIcon}
|
||||
unread={categoryUnreadCount(root, infrequentThresholdMs)}
|
||||
hasNewEntries={categoryHasNewEntries(root, infrequentThresholdMs)}
|
||||
selected={source.type === "category" && source.id === Constants.categories.infrequent.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
hasError={false}
|
||||
hasWarning={false}
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
|
||||
const categoryNode = (category: Category, level = 0) => {
|
||||
if (!isCategoryDisplayed(category)) return null
|
||||
@@ -197,6 +218,7 @@ export function Tree() {
|
||||
<Box className="cf-tree">
|
||||
{allCategoryNode()}
|
||||
{starredCategoryNode()}
|
||||
{infrequentCategoryNode()}
|
||||
{root.children.map(c => recursiveCategoryNode(c))}
|
||||
{root.feeds.map(f => feedNode(f))}
|
||||
{tags?.map(tag => tagNode(tag))}
|
||||
|
||||
@@ -405,6 +405,10 @@ msgstr "Feed name"
|
||||
msgid "Feed URL"
|
||||
msgstr "Feed URL"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Feeds posting less often than this (on average) will appear in the Infrequent view"
|
||||
msgstr "Feeds posting less often than this (on average) will appear in the Infrequent view"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr "Fetch all my feeds now"
|
||||
@@ -502,6 +506,14 @@ msgstr "In expanded view, scrolling through entries mark them as read"
|
||||
msgid "Indigo"
|
||||
msgstr "Indigo"
|
||||
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Infrequent"
|
||||
msgstr "Infrequent"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Infrequent posts threshold (days)"
|
||||
msgstr "Infrequent posts threshold (days)"
|
||||
|
||||
#: src/pages/auth/InitialSetupPage.tsx
|
||||
msgid "Initial Setup"
|
||||
msgstr "Initial Setup"
|
||||
|
||||
Reference in New Issue
Block a user