forked from Archives/Athou_commafeed
Compare commits
1 Commits
master
...
renovate/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e331f363f0 |
@@ -1,22 +0,0 @@
|
|||||||
# `garrettmills/commafeed`
|
|
||||||
|
|
||||||
This is my personal fork of `Athou/commafeed` with some tweaks:
|
|
||||||
|
|
||||||
- "Infrequent" tab - like "All" but limits to blogs w/ an average post interval greater than a user-configurable number of days
|
|
||||||
- User preference to disable the swipe-to-open-menu gesture on mobile
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
Use `gmfork-build-docker.sh` to build the JVM Docker image for `linux/amd64`:
|
|
||||||
|
|
||||||
You can use the `DB_VARIANT` env var to change which DB the image builds with. By default, it builds the `postgresql` variant.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
DOCKER_REGISTRY=myregistry.example.com DB_VARIANT=h2 ./gmfork-build-docker.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
To run locally:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -p 8082:8082 $DOCKER_REGISTRY/commafeed-fork:latest
|
|
||||||
```
|
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
"@fontsource/open-sans": "^5.2.7",
|
"@fontsource/open-sans": "^5.2.7",
|
||||||
"@lingui/core": "^5.9.3",
|
"@lingui/core": "^5.9.3",
|
||||||
"@lingui/react": "^5.9.3",
|
"@lingui/react": "^5.9.3",
|
||||||
"@mantine/core": "^8.3.16",
|
"@mantine/core": "^8.3.18",
|
||||||
"@mantine/form": "^8.3.16",
|
"@mantine/form": "^8.3.18",
|
||||||
"@mantine/hooks": "^8.3.16",
|
"@mantine/hooks": "^8.3.18",
|
||||||
"@mantine/modals": "^8.3.16",
|
"@mantine/modals": "^8.3.18",
|
||||||
"@mantine/notifications": "^8.3.16",
|
"@mantine/notifications": "^8.3.18",
|
||||||
"@mantine/spotlight": "^8.3.16",
|
"@mantine/spotlight": "^8.3.18",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@react-querybuilder/mantine": "^8.14.0",
|
"@react-querybuilder/mantine": "^8.14.0",
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
|
|||||||
@@ -18,13 +18,6 @@ const categories: Record<string, Omit<Category, "name">> = {
|
|||||||
feeds: [],
|
feeds: [],
|
||||||
position: 1,
|
position: 1,
|
||||||
},
|
},
|
||||||
infrequent: {
|
|
||||||
id: "infrequent",
|
|
||||||
expanded: false,
|
|
||||||
children: [],
|
|
||||||
feeds: [],
|
|
||||||
position: 2,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharing: {
|
const sharing: {
|
||||||
@@ -112,7 +105,6 @@ export const Constants = {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
delay: 500,
|
delay: 500,
|
||||||
},
|
},
|
||||||
infrequentThresholdDaysDefault: 7,
|
|
||||||
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
||||||
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
|
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
|
||||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export interface Subscription {
|
|||||||
filterLegacy?: string
|
filterLegacy?: string
|
||||||
pushNotificationsEnabled: boolean
|
pushNotificationsEnabled: boolean
|
||||||
autoMarkAsReadAfterDays?: number
|
autoMarkAsReadAfterDays?: number
|
||||||
averageEntryIntervalMs?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
@@ -285,8 +284,6 @@ export interface Settings {
|
|||||||
unreadCountTitle: boolean
|
unreadCountTitle: boolean
|
||||||
unreadCountFavicon: boolean
|
unreadCountFavicon: boolean
|
||||||
disablePullToRefresh: boolean
|
disablePullToRefresh: boolean
|
||||||
disableMobileSwipe: boolean
|
|
||||||
infrequentThresholdDays: number
|
|
||||||
primaryColor?: string
|
primaryColor?: string
|
||||||
sharingSettings: SharingSettings
|
sharingSettings: SharingSettings
|
||||||
pushNotificationSettings: PushNotificationSettings
|
pushNotificationSettings: PushNotificationSettings
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import { createSlice, isAnyOf, type PayloadAction } from "@reduxjs/toolkit"
|
|||||||
import type { LocalSettings, Settings, UserModel, ViewMode } from "@/app/types"
|
import type { LocalSettings, Settings, UserModel, ViewMode } from "@/app/types"
|
||||||
import {
|
import {
|
||||||
changeCustomContextMenu,
|
changeCustomContextMenu,
|
||||||
changeDisableMobileSwipe,
|
|
||||||
changeDisablePullToRefresh,
|
changeDisablePullToRefresh,
|
||||||
changeEntriesToKeepOnTopWhenScrolling,
|
changeEntriesToKeepOnTopWhenScrolling,
|
||||||
changeExternalLinkIconDisplayMode,
|
changeExternalLinkIconDisplayMode,
|
||||||
changeInfrequentThresholdDays,
|
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
changeMarkAllAsReadConfirmation,
|
changeMarkAllAsReadConfirmation,
|
||||||
changeMarkAllAsReadNavigateToUnread,
|
changeMarkAllAsReadNavigateToUnread,
|
||||||
@@ -143,14 +141,6 @@ export const userSlice = createSlice({
|
|||||||
if (!state.settings) return
|
if (!state.settings) return
|
||||||
state.settings.disablePullToRefresh = action.meta.arg
|
state.settings.disablePullToRefresh = action.meta.arg
|
||||||
})
|
})
|
||||||
builder.addCase(changeDisableMobileSwipe.pending, (state, action) => {
|
|
||||||
if (!state.settings) return
|
|
||||||
state.settings.disableMobileSwipe = 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) => {
|
builder.addCase(changePrimaryColor.pending, (state, action) => {
|
||||||
if (!state.settings) return
|
if (!state.settings) return
|
||||||
state.settings.primaryColor = action.meta.arg
|
state.settings.primaryColor = action.meta.arg
|
||||||
@@ -181,8 +171,6 @@ export const userSlice = createSlice({
|
|||||||
changeUnreadCountTitle.fulfilled,
|
changeUnreadCountTitle.fulfilled,
|
||||||
changeUnreadCountFavicon.fulfilled,
|
changeUnreadCountFavicon.fulfilled,
|
||||||
changeDisablePullToRefresh.fulfilled,
|
changeDisablePullToRefresh.fulfilled,
|
||||||
changeDisableMobileSwipe.fulfilled,
|
|
||||||
changeInfrequentThresholdDays.fulfilled,
|
|
||||||
changePrimaryColor.fulfilled,
|
changePrimaryColor.fulfilled,
|
||||||
changeSharingSetting.fulfilled,
|
changeSharingSetting.fulfilled,
|
||||||
changePushNotificationSettings.fulfilled
|
changePushNotificationSettings.fulfilled
|
||||||
|
|||||||
@@ -131,12 +131,6 @@ export const changeDisablePullToRefresh = createAppAsyncThunk(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const changeDisableMobileSwipe = createAppAsyncThunk("settings/disableMobileSwipe", (disableMobileSwipe: boolean, thunkApi) => {
|
|
||||||
const { settings } = thunkApi.getState().user
|
|
||||||
if (!settings) return
|
|
||||||
client.user.saveSettings({ ...settings, disableMobileSwipe })
|
|
||||||
})
|
|
||||||
|
|
||||||
export const changePrimaryColor = createAppAsyncThunk("settings/primaryColor", (primaryColor: string, thunkApi) => {
|
export const changePrimaryColor = createAppAsyncThunk("settings/primaryColor", (primaryColor: string, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
@@ -164,15 +158,6 @@ 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(
|
export const changePushNotificationSettings = createAppAsyncThunk(
|
||||||
"settings/pushNotificationSettings",
|
"settings/pushNotificationSettings",
|
||||||
(pushNotificationSettings: PushNotificationSettings, thunkApi) => {
|
(pushNotificationSettings: PushNotificationSettings, thunkApi) => {
|
||||||
|
|||||||
@@ -26,22 +26,20 @@ export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
|
|||||||
return categories
|
return categories
|
||||||
}
|
}
|
||||||
|
|
||||||
export function categoryUnreadCount(category?: TreeCategory, maxFrequencyThresholdMs?: number): number {
|
export function categoryUnreadCount(category?: TreeCategory): number {
|
||||||
if (!category) return 0
|
if (!category) return 0
|
||||||
|
|
||||||
return flattenCategoryTree(category)
|
return flattenCategoryTree(category)
|
||||||
.flatMap(c => c.feeds)
|
.flatMap(c => c.feeds)
|
||||||
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
|
|
||||||
.map(f => f.unread)
|
.map(f => f.unread)
|
||||||
.reduce((total, current) => total + current, 0)
|
.reduce((total, current) => total + current, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function categoryHasNewEntries(category?: TreeCategory, maxFrequencyThresholdMs?: number): boolean {
|
export function categoryHasNewEntries(category?: TreeCategory): boolean {
|
||||||
if (!category) return false
|
if (!category) return false
|
||||||
|
|
||||||
return flattenCategoryTree(category)
|
return flattenCategoryTree(category)
|
||||||
.flatMap(c => c.feeds)
|
.flatMap(c => c.feeds)
|
||||||
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
|
|
||||||
.some(f => f.hasNewEntries)
|
.some(f => f.hasNewEntries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import { useAppDispatch, useAppSelector } from "@/app/store"
|
|||||||
import type { IconDisplayMode, ScrollMode, SharingSettings } from "@/app/types"
|
import type { IconDisplayMode, ScrollMode, SharingSettings } from "@/app/types"
|
||||||
import {
|
import {
|
||||||
changeCustomContextMenu,
|
changeCustomContextMenu,
|
||||||
changeDisableMobileSwipe,
|
|
||||||
changeDisablePullToRefresh,
|
changeDisablePullToRefresh,
|
||||||
changeEntriesToKeepOnTopWhenScrolling,
|
changeEntriesToKeepOnTopWhenScrolling,
|
||||||
changeExternalLinkIconDisplayMode,
|
changeExternalLinkIconDisplayMode,
|
||||||
changeInfrequentThresholdDays,
|
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
changeMarkAllAsReadConfirmation,
|
changeMarkAllAsReadConfirmation,
|
||||||
changeMarkAllAsReadNavigateToUnread,
|
changeMarkAllAsReadNavigateToUnread,
|
||||||
@@ -46,8 +44,6 @@ export function DisplaySettings() {
|
|||||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||||
const disablePullToRefresh = useAppSelector(state => state.user.settings?.disablePullToRefresh)
|
const disablePullToRefresh = useAppSelector(state => state.user.settings?.disablePullToRefresh)
|
||||||
const disableMobileSwipe = useAppSelector(state => state.user.settings?.disableMobileSwipe)
|
|
||||||
const infrequentThresholdDays = useAppSelector(state => state.user.settings?.infrequentThresholdDays)
|
|
||||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||||
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
|
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
|
||||||
const { _ } = useLingui()
|
const { _ } = useLingui()
|
||||||
@@ -147,20 +143,6 @@ export function DisplaySettings() {
|
|||||||
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
|
||||||
label={<Trans>On mobile, disable swipe gesture to open the menu</Trans>}
|
|
||||||
checked={disableMobileSwipe}
|
|
||||||
onChange={async e => await dispatch(changeDisableMobileSwipe(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" />
|
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Trans } from "@lingui/react/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { Box, Stack } from "@mantine/core"
|
import { Box, Stack } from "@mantine/core"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { TbChevronDown, TbChevronRight, TbClock, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||||
import { Constants } from "@/app/constants"
|
import { Constants } from "@/app/constants"
|
||||||
import {
|
import {
|
||||||
redirectToCategory,
|
redirectToCategory,
|
||||||
@@ -23,7 +23,6 @@ import { TreeSearch } from "./TreeSearch"
|
|||||||
|
|
||||||
const allIcon = <TbInbox size={16} />
|
const allIcon = <TbInbox size={16} />
|
||||||
const starredIcon = <TbStar size={16} />
|
const starredIcon = <TbStar size={16} />
|
||||||
const infrequentIcon = <TbClock size={16} />
|
|
||||||
const tagIcon = <TbTag size={16} />
|
const tagIcon = <TbTag size={16} />
|
||||||
const expandedIcon = <TbChevronDown size={16} />
|
const expandedIcon = <TbChevronDown size={16} />
|
||||||
const collapsedIcon = <TbChevronRight size={16} />
|
const collapsedIcon = <TbChevronRight size={16} />
|
||||||
@@ -35,10 +34,6 @@ export function Tree() {
|
|||||||
const source = useAppSelector(state => state.entries.source)
|
const source = useAppSelector(state => state.entries.source)
|
||||||
const tags = useAppSelector(state => state.user.tags)
|
const tags = useAppSelector(state => state.user.tags)
|
||||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
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 dispatch = useAppDispatch()
|
||||||
|
|
||||||
const isFeedDisplayed = (feed: Subscription) => {
|
const isFeedDisplayed = (feed: Subscription) => {
|
||||||
@@ -120,22 +115,6 @@ export function Tree() {
|
|||||||
onClick={categoryClicked}
|
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) => {
|
const categoryNode = (category: Category, level = 0) => {
|
||||||
if (!isCategoryDisplayed(category)) return null
|
if (!isCategoryDisplayed(category)) return null
|
||||||
@@ -218,7 +197,6 @@ export function Tree() {
|
|||||||
<Box className="cf-tree">
|
<Box className="cf-tree">
|
||||||
{allCategoryNode()}
|
{allCategoryNode()}
|
||||||
{starredCategoryNode()}
|
{starredCategoryNode()}
|
||||||
{infrequentCategoryNode()}
|
|
||||||
{root.children.map(c => recursiveCategoryNode(c))}
|
{root.children.map(c => recursiveCategoryNode(c))}
|
||||||
{root.feeds.map(f => feedNode(f))}
|
{root.feeds.map(f => feedNode(f))}
|
||||||
{tags?.map(tag => tagNode(tag))}
|
{tags?.map(tag => tagNode(tag))}
|
||||||
|
|||||||
@@ -405,10 +405,6 @@ msgstr "Feed name"
|
|||||||
msgid "Feed URL"
|
msgid "Feed URL"
|
||||||
msgstr "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
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr "Fetch all my feeds now"
|
msgstr "Fetch all my feeds now"
|
||||||
@@ -506,14 +502,6 @@ msgstr "In expanded view, scrolling through entries mark them as read"
|
|||||||
msgid "Indigo"
|
msgid "Indigo"
|
||||||
msgstr "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
|
#: src/pages/auth/InitialSetupPage.tsx
|
||||||
msgid "Initial Setup"
|
msgid "Initial Setup"
|
||||||
msgstr "Initial Setup"
|
msgstr "Initial Setup"
|
||||||
@@ -715,10 +703,6 @@ msgstr "On desktop"
|
|||||||
msgid "On mobile"
|
msgid "On mobile"
|
||||||
msgstr "On mobile"
|
msgstr "On mobile"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
|
||||||
msgid "On mobile, disable swipe gesture to open the menu"
|
|
||||||
msgstr "On mobile, disable swipe gesture to open the menu"
|
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||||
msgstr "On mobile, show action buttons at the bottom of the screen"
|
msgstr "On mobile, show action buttons at the bottom of the screen"
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export default function Layout(props: Readonly<LayoutProps>) {
|
|||||||
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
|
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
|
||||||
const treeReloadInterval = useAppSelector(state => state.server.serverInfos?.treeReloadInterval)
|
const treeReloadInterval = useAppSelector(state => state.server.serverInfos?.treeReloadInterval)
|
||||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||||
const disableMobileSwipe = useAppSelector(state => state.user.settings?.disableMobileSwipe)
|
|
||||||
const sidebarWidth = useAppSelector(state => state.user.localSettings.sidebarWidth)
|
const sidebarWidth = useAppSelector(state => state.user.localSettings.sidebarWidth)
|
||||||
const headerInFooter = mobile && !isBrowserExtensionPopup && mobileFooter
|
const headerInFooter = mobile && !isBrowserExtensionPopup && mobileFooter
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -165,9 +164,6 @@ export default function Layout(props: Readonly<LayoutProps>) {
|
|||||||
|
|
||||||
const swipeHandlers = useSwipeable({
|
const swipeHandlers = useSwipeable({
|
||||||
onSwiping: e => {
|
onSwiping: e => {
|
||||||
if (disableMobileSwipe) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const threshold = document.documentElement.clientWidth / 6
|
const threshold = document.documentElement.clientWidth / 6
|
||||||
if (e.absX > threshold) {
|
if (e.absX > threshold) {
|
||||||
dispatch(setMobileMenuOpen(e.dir === "Right"))
|
dispatch(setMobileMenuOpen(e.dir === "Right"))
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public class Feed extends AbstractModel {
|
|||||||
private String etagHeader;
|
private String etagHeader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* average time between entries in the feed in milliseconds
|
* average time between entries in the feed
|
||||||
*/
|
*/
|
||||||
private Long averageEntryInterval;
|
private Long averageEntryInterval;
|
||||||
|
|
||||||
|
|||||||
@@ -145,9 +145,6 @@ public class UserSettings extends AbstractModel {
|
|||||||
private boolean unreadCountTitle;
|
private boolean unreadCountTitle;
|
||||||
private boolean unreadCountFavicon;
|
private boolean unreadCountFavicon;
|
||||||
private boolean disablePullToRefresh;
|
private boolean disablePullToRefresh;
|
||||||
private boolean disableMobileSwipe;
|
|
||||||
|
|
||||||
private int infrequentThresholdDays;
|
|
||||||
|
|
||||||
private boolean email;
|
private boolean email;
|
||||||
private boolean gmail;
|
private boolean gmail;
|
||||||
|
|||||||
@@ -76,12 +76,6 @@ public class Settings implements Serializable {
|
|||||||
@Schema(description = "disable pull to refresh", required = true)
|
@Schema(description = "disable pull to refresh", required = true)
|
||||||
private boolean disablePullToRefresh;
|
private boolean disablePullToRefresh;
|
||||||
|
|
||||||
@Schema(description = "disable swipe gesture to open mobile menu", required = true)
|
|
||||||
private boolean disableMobileSwipe;
|
|
||||||
|
|
||||||
@Schema(description = "threshold in days for the infrequent view", required = true)
|
|
||||||
private int infrequentThresholdDays;
|
|
||||||
|
|
||||||
@Schema(description = "primary theme color to use in the UI")
|
@Schema(description = "primary theme color to use in the UI")
|
||||||
private String primaryColor;
|
private String primaryColor;
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ public class Subscription implements Serializable {
|
|||||||
@Schema(description = "automatically mark entries as read after this many days (null to disable)")
|
@Schema(description = "automatically mark entries as read after this many days (null to disable)")
|
||||||
private Integer autoMarkAsReadAfterDays;
|
private Integer autoMarkAsReadAfterDays;
|
||||||
|
|
||||||
@Schema(description = "average time in milliseconds between entries in this feed, null if unknown")
|
|
||||||
private Long averageEntryIntervalMs;
|
|
||||||
|
|
||||||
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
|
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
|
||||||
FeedCategory category = subscription.getCategory();
|
FeedCategory category = subscription.getCategory();
|
||||||
Feed feed = subscription.getFeed();
|
Feed feed = subscription.getFeed();
|
||||||
@@ -96,7 +93,6 @@ public class Subscription implements Serializable {
|
|||||||
sub.setFilterLegacy(subscription.getFilterLegacy());
|
sub.setFilterLegacy(subscription.getFilterLegacy());
|
||||||
sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
|
sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
|
||||||
sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays());
|
sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays());
|
||||||
sub.setAverageEntryIntervalMs(feed.getAverageEntryInterval());
|
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,14 +40,12 @@ import com.commafeed.CommaFeedConfiguration;
|
|||||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
|
||||||
import com.commafeed.backend.feed.FeedEntryKeyword;
|
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||||
import com.commafeed.backend.feed.FeedUtils;
|
import com.commafeed.backend.feed.FeedUtils;
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.model.UserSettings;
|
|
||||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||||
import com.commafeed.backend.service.FeedEntryService;
|
import com.commafeed.backend.service.FeedEntryService;
|
||||||
@@ -85,7 +83,6 @@ public class CategoryREST {
|
|||||||
|
|
||||||
public static final String ALL = "all";
|
public static final String ALL = "all";
|
||||||
public static final String STARRED = "starred";
|
public static final String STARRED = "starred";
|
||||||
public static final String INFREQUENT = "infrequent";
|
|
||||||
|
|
||||||
private final AuthenticationContext authenticationContext;
|
private final AuthenticationContext authenticationContext;
|
||||||
private final FeedCategoryDAO feedCategoryDAO;
|
private final FeedCategoryDAO feedCategoryDAO;
|
||||||
@@ -93,7 +90,6 @@ public class CategoryREST {
|
|||||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||||
private final FeedEntryService feedEntryService;
|
private final FeedEntryService feedEntryService;
|
||||||
private final FeedSubscriptionService feedSubscriptionService;
|
private final FeedSubscriptionService feedSubscriptionService;
|
||||||
private final UserSettingsDAO userSettingsDAO;
|
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
private final UriInfo uri;
|
private final UriInfo uri;
|
||||||
|
|
||||||
@@ -143,15 +139,11 @@ public class CategoryREST {
|
|||||||
}
|
}
|
||||||
|
|
||||||
User user = authenticationContext.getCurrentUser();
|
User user = authenticationContext.getCurrentUser();
|
||||||
if (ALL.equals(id) || INFREQUENT.equals(id)) {
|
if (ALL.equals(id)) {
|
||||||
entries.setName(Optional.ofNullable(tag).orElse("All"));
|
entries.setName(Optional.ofNullable(tag).orElse("All"));
|
||||||
|
|
||||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||||
removeExcludedSubscriptions(subs, excludedIds);
|
removeExcludedSubscriptions(subs, excludedIds);
|
||||||
if (INFREQUENT.equals(id)) {
|
|
||||||
entries.setName("Infrequent");
|
|
||||||
removeFrequentSubscriptions(subs, user);
|
|
||||||
}
|
|
||||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
|
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
|
||||||
offset, limit + 1, order, true, tag, null, null);
|
offset, limit + 1, order, true, tag, null, null);
|
||||||
|
|
||||||
@@ -252,12 +244,9 @@ public class CategoryREST {
|
|||||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||||
|
|
||||||
User user = authenticationContext.getCurrentUser();
|
User user = authenticationContext.getCurrentUser();
|
||||||
if (ALL.equals(req.getId()) || INFREQUENT.equals(req.getId())) {
|
if (ALL.equals(req.getId())) {
|
||||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||||
if (INFREQUENT.equals(req.getId())) {
|
|
||||||
removeFrequentSubscriptions(subs, user);
|
|
||||||
}
|
|
||||||
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
|
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
|
||||||
} else if (STARRED.equals(req.getId())) {
|
} else if (STARRED.equals(req.getId())) {
|
||||||
feedEntryService.markStarredEntries(user, olderThan, insertedBefore);
|
feedEntryService.markStarredEntries(user, olderThan, insertedBefore);
|
||||||
@@ -271,17 +260,6 @@ public class CategoryREST {
|
|||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeFrequentSubscriptions(List<FeedSubscription> subs, User user) {
|
|
||||||
UserSettings userSettings = userSettingsDAO.findByUser(user);
|
|
||||||
int infrequentDays = userSettings != null && userSettings.getInfrequentThresholdDays() > 0
|
|
||||||
? userSettings.getInfrequentThresholdDays()
|
|
||||||
: 7;
|
|
||||||
long infrequentThresholdMs = (long) infrequentDays * 24 * 3600 * 1000;
|
|
||||||
|
|
||||||
subs.removeIf(
|
|
||||||
sub -> sub.getFeed().getAverageEntryInterval() == null || sub.getFeed().getAverageEntryInterval() < infrequentThresholdMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeExcludedSubscriptions(List<FeedSubscription> subs, List<Long> excludedIds) {
|
private void removeExcludedSubscriptions(List<FeedSubscription> subs, List<Long> excludedIds) {
|
||||||
if (CollectionUtils.isNotEmpty(excludedIds)) {
|
if (CollectionUtils.isNotEmpty(excludedIds)) {
|
||||||
subs.removeIf(sub -> excludedIds.contains(sub.getId()));
|
subs.removeIf(sub -> excludedIds.contains(sub.getId()));
|
||||||
|
|||||||
@@ -132,9 +132,7 @@ public class UserREST {
|
|||||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||||
s.setDisableMobileSwipe(settings.isDisableMobileSwipe());
|
|
||||||
s.setPrimaryColor(settings.getPrimaryColor());
|
s.setPrimaryColor(settings.getPrimaryColor());
|
||||||
s.setInfrequentThresholdDays(settings.getInfrequentThresholdDays());
|
|
||||||
|
|
||||||
if (settings.getPushNotifications() != null) {
|
if (settings.getPushNotifications() != null) {
|
||||||
s.getPushNotificationSettings().setType(settings.getPushNotifications().getType());
|
s.getPushNotificationSettings().setType(settings.getPushNotifications().getType());
|
||||||
@@ -170,8 +168,6 @@ public class UserREST {
|
|||||||
s.setUnreadCountTitle(false);
|
s.setUnreadCountTitle(false);
|
||||||
s.setUnreadCountFavicon(true);
|
s.setUnreadCountFavicon(true);
|
||||||
s.setDisablePullToRefresh(false);
|
s.setDisablePullToRefresh(false);
|
||||||
s.setDisableMobileSwipe(false);
|
|
||||||
s.setInfrequentThresholdDays(7);
|
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -208,9 +204,7 @@ public class UserREST {
|
|||||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||||
s.setDisableMobileSwipe(settings.isDisableMobileSwipe());
|
|
||||||
s.setPrimaryColor(settings.getPrimaryColor());
|
s.setPrimaryColor(settings.getPrimaryColor());
|
||||||
s.setInfrequentThresholdDays(settings.getInfrequentThresholdDays());
|
|
||||||
|
|
||||||
PushNotificationUserSettings ps = new PushNotificationUserSettings();
|
PushNotificationUserSettings ps = new PushNotificationUserSettings();
|
||||||
ps.setType(settings.getPushNotificationSettings().getType());
|
ps.setType(settings.getPushNotificationSettings().getType());
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
|
||||||
|
|
||||||
<changeSet id="add-infrequent-days-threshold" author="athou">
|
|
||||||
<addColumn tableName="USERSETTINGS">
|
|
||||||
<column name="infrequentThresholdDays" type="INT" valueNumeric="7">
|
|
||||||
<constraints nullable="false" />
|
|
||||||
</column>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
<changeSet id="add-disable-mobile-swipe" author="athou">
|
|
||||||
<addColumn tableName="USERSETTINGS">
|
|
||||||
<column name="disableMobileSwipe" type="BOOLEAN" valueBoolean="false">
|
|
||||||
<constraints nullable="false" />
|
|
||||||
</column>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
@@ -38,6 +38,5 @@
|
|||||||
<include file="changelogs/db.changelog-5.11.xml" />
|
<include file="changelogs/db.changelog-5.11.xml" />
|
||||||
<include file="changelogs/db.changelog-5.12.xml" />
|
<include file="changelogs/db.changelog-5.12.xml" />
|
||||||
<include file="changelogs/db.changelog-7.0.xml" />
|
<include file="changelogs/db.changelog-7.0.xml" />
|
||||||
<include file="changelogs/db.changelog-gmfork.xml" />
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DB_VARIANT="${DB_VARIANT:-postgresql}"
|
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
ARTIFACTS_DIR="$REPO_ROOT/artifacts"
|
|
||||||
|
|
||||||
if [ -z "${DOCKER_REGISTRY:-}" ]; then
|
|
||||||
echo "Error: DOCKER_REGISTRY is not set" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
./mvnw --batch-mode --no-transfer-progress install -P${DB_VARIANT} -DskipTests
|
|
||||||
|
|
||||||
# Prepare artifacts
|
|
||||||
rm -rf "$ARTIFACTS_DIR"
|
|
||||||
mkdir -p "$ARTIFACTS_DIR"
|
|
||||||
|
|
||||||
cp commafeed-server/target/commafeed-*-${DB_VARIANT}-jvm.zip "$ARTIFACTS_DIR/"
|
|
||||||
unzip -q "$ARTIFACTS_DIR"/*-${DB_VARIANT}-jvm.zip -d "$ARTIFACTS_DIR/extracted-jvm-package"
|
|
||||||
mv "$ARTIFACTS_DIR/extracted-jvm-package"/commafeed-* "$ARTIFACTS_DIR/extracted-jvm-package/quarkus-app"
|
|
||||||
|
|
||||||
# Build image
|
|
||||||
docker build \
|
|
||||||
--platform linux/amd64 \
|
|
||||||
--file commafeed-server/src/main/docker/Dockerfile.jvm \
|
|
||||||
--tag "$DOCKER_REGISTRY/commafeed-fork:latest" \
|
|
||||||
.
|
|
||||||
|
|
||||||
rm -rf "$ARTIFACTS_DIR"
|
|
||||||
|
|
||||||
echo "Built: $DOCKER_REGISTRY/commafeed-fork:latest"
|
|
||||||
Reference in New Issue
Block a user