Add Infrequent tab and corresponding user setting

This commit is contained in:
2026-03-17 21:38:17 -05:00
parent d05c5b9d7f
commit e2a1630adc
16 changed files with 126 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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