Compare commits

...

47 Commits
5.2.0 ... 5.3.1

Author SHA1 Message Date
Athou
74bce1308c release 5.3.1 2024-10-04 20:33:56 +02:00
Athou
98cfa6d2c8 add regression test 2024-10-04 20:24:43 +02:00
Athou
99a02a2186 fix issue with some HTTP feeds (#1572) 2024-10-04 20:20:02 +02:00
Jérémie Panzer
3431a813b1 Merge pull request #1574 from Athou/renovate/npm-10.x
chore(deps): update dependency npm to v10.9.0
2024-10-04 07:51:15 +02:00
renovate[bot]
e9e0e8d32b chore(deps): update dependency npm to v10.9.0 2024-10-03 19:31:06 +00:00
renovate[bot]
2d14409d35 fix(deps): update dependency io.dropwizard.metrics:metrics-json to v4.2.28 2024-10-03 19:31:04 +00:00
Jérémie Panzer
a8200e5c58 Merge pull request #1573 from Athou/renovate/node-20.x
chore(deps): update dependency node to v20.18.0
2024-10-03 21:30:44 +02:00
renovate[bot]
79a8df8b06 chore(deps): update dependency node to v20.18.0 2024-10-03 19:00:34 +00:00
renovate[bot]
061a5f0262 fix(deps): update mantine monorepo to ^7.13.2 2024-10-03 13:32:54 +00:00
renovate[bot]
821bdb3b0f chore(deps): update dependency vitest to ^2.1.2 2024-10-02 21:46:25 +00:00
renovate[bot]
606dfa9299 chore(deps): update dependency @types/react to ^18.3.11 2024-10-02 18:42:46 +00:00
renovate[bot]
131357c616 fix(deps): update swagger.version to v2.2.25 2024-10-02 13:17:53 +00:00
renovate[bot]
f6d3493bad chore(deps): update dependency @biomejs/biome to v1.9.3 2024-10-01 14:28:45 +00:00
renovate[bot]
0c6104e25b fix(deps): update mantine monorepo to ^7.13.1 2024-09-30 09:40:08 +00:00
Jérémie Panzer
d73735a35d Merge pull request #1571 from Athou/renovate/vitejs-plugin-react-4.3.x
chore(deps): update dependency @vitejs/plugin-react to ^4.3.2
2024-09-30 06:31:07 +02:00
renovate[bot]
e725d2d6b6 chore(deps): update dependency @vitejs/plugin-react to ^4.3.2 2024-09-30 04:02:46 +00:00
renovate[bot]
f0e1279d68 chore(deps): lock file maintenance 2024-09-30 00:36:34 +00:00
renovate[bot]
c74c74d2c4 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.18.2 2024-09-29 19:11:52 +00:00
renovate[bot]
aa70cf5dcd chore(deps): update dependency @types/react to ^18.3.10 2024-09-27 16:41:55 +00:00
Jérémie Panzer
1055259627 Merge pull request #1569 from canoine/patch-1
Update fr/messages.po
2024-09-26 22:35:33 +02:00
canoine
302d37b6ef Update fr/messages.po
French translation update
2024-09-26 21:41:37 +02:00
Jérémie Panzer
8532a73d94 Merge pull request #1565 from Athou/renovate/quarkus.version
chore(deps): update quarkus.version to v3.15.1 (minor)
2024-09-26 10:25:09 +02:00
Athou
ffafb272cb update docs 2024-09-26 10:08:52 +02:00
Jérémie Panzer
22e0171a34 Merge pull request #1566 from dai/master
chore(ja): translated some strings
2024-09-26 10:03:32 +02:00
renovate[bot]
2b410f040c Update quarkus.version to v3.15.1 2024-09-26 08:02:28 +00:00
dai
259e8ad4e5 chore: translated some strings
Chore: Translated some new strings and reworked some wording.
2024-09-26 14:25:19 +09:00
Jérémie Panzer
21244dd9f5 Merge pull request #1564 from Athou/renovate/mantine-monorepo
Update mantine monorepo to ^7.13.0 (minor)
2024-09-25 12:52:53 +02:00
renovate[bot]
bc6206180d Update mantine monorepo to ^7.13.0 2024-09-25 09:53:00 +00:00
renovate[bot]
6e22d21358 Update dependency vite to ^5.4.8 2024-09-25 05:04:17 +00:00
renovate[bot]
95bdb4e700 Update dependency @types/react to ^18.3.9 2024-09-24 15:24:13 +00:00
Athou
9b7dbc68ab release 5.3.0 2024-09-24 07:58:43 +02:00
Athou
dc86c9b0db also manually load more entries if needed when pressing the next entry button in the header (#1557) 2024-09-24 07:54:50 +02:00
renovate[bot]
cb92ed753f Update swagger.version to v2.2.24 2024-09-23 16:36:50 +00:00
renovate[bot]
10a085e24e Lock file maintenance 2024-09-23 00:08:26 +00:00
renovate[bot]
3400a39edf Update dependency jsdom to ^25.0.1 2024-09-22 07:08:58 +00:00
Athou
21efffa345 Update dependency io.github.hakky54:sslcontext-kickstart-for-apache5 to v8.3.7
Update dependency org.apache.httpcomponents.client5:httpclient5 to v5.4
2024-09-22 09:07:20 +02:00
renovate[bot]
e2e80ba7e5 Update dependency com.github.eirslett:frontend-maven-plugin to v1.15.1 2024-09-21 18:11:14 +00:00
Jérémie Panzer
d988dba66e Merge pull request #1563 from Athou/renovate/querydsl.version
Update querydsl.version to v6.8 (minor)
2024-09-21 15:42:21 +02:00
renovate[bot]
403201fbff Update querydsl.version to v6.8 2024-09-21 13:25:28 +00:00
Athou
3cc93b51bb set default cooldown duration to 0 so it's not a breaking change 2024-09-21 10:57:16 +02:00
Athou
6a7d83bb45 show an error if force fetching feeds is not yet available 2024-09-21 10:31:17 +02:00
Athou
19c8db8b31 add a cooldown on the force refresh action (#1556) 2024-09-21 08:24:14 +02:00
renovate[bot]
0d75688ec8 Update dependency vite to ^5.4.7 2024-09-20 16:39:11 +00:00
renovate[bot]
e01dcb2f5b Update dependency @types/react to ^18.3.8 2024-09-19 22:25:45 +00:00
Jérémie Panzer
57757e2c14 Merge pull request #1561 from Athou/renovate/monaco-editor-0.x
Update dependency monaco-editor to ^0.52.0
2024-09-19 17:59:58 +02:00
renovate[bot]
779cd2fcfe Update dependency monaco-editor to ^0.52.0 2024-09-19 13:51:05 +00:00
renovate[bot]
94919f22e4 Update dependency @biomejs/biome to v1.9.2 2024-09-19 13:51:00 +00:00
60 changed files with 672 additions and 456 deletions

View File

@@ -1,5 +1,14 @@
# Changelog
## [5.3.1]
- Fixed an issue that could cause some HTTP feeds to return a 400 error (#1572)
## [5.3.0]
- Added a setting to set a cooldown on the "fetch all my feeds" action, disabled by default (#1556)
- Fixed an issue that could cause entries to not correctly load when using the "next" header button (#1557)
## [5.2.0]
- Added an option to keep a number of entries above the selected entry when scrolling

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.1/schema.json",
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
"formatter": {
"indentStyle": "space",
"indentWidth": 4,

File diff suppressed because it is too large Load Diff

View File

@@ -20,19 +20,19 @@
"@lingui/core": "^4.11.4",
"@lingui/macro": "^4.11.4",
"@lingui/react": "^4.11.4",
"@mantine/core": "^7.12.2",
"@mantine/form": "^7.12.2",
"@mantine/hooks": "^7.12.2",
"@mantine/modals": "^7.12.2",
"@mantine/notifications": "^7.12.2",
"@mantine/spotlight": "^7.12.2",
"@mantine/core": "^7.13.2",
"@mantine/form": "^7.13.2",
"@mantine/hooks": "^7.13.2",
"@mantine/modals": "^7.13.2",
"@mantine/notifications": "^7.13.2",
"@mantine/spotlight": "^7.13.2",
"@monaco-editor/react": "^4.6.0",
"@reduxjs/toolkit": "^2.2.7",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0",
"monaco-editor": "^0.51.0",
"monaco-editor": "^0.52.0",
"mousetrap": "^1.6.5",
"react": "^18.3.1",
"react-async-hook": "^4.0.0",
@@ -54,26 +54,26 @@
"websocket-heartbeat-js": "^1.1.3"
},
"devDependencies": {
"@biomejs/biome": "^1.9.1",
"@biomejs/biome": "^1.9.3",
"@lingui/cli": "^4.11.4",
"@lingui/vite-plugin": "^4.11.4",
"@types/mousetrap": "^1.6.15",
"@types/react": "^18.3.7",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11",
"@types/react-infinite-scroller": "^1.2.5",
"@types/swagger-ui-react": "^4.18.3",
"@types/throttle-debounce": "^5.0.2",
"@types/tinycon": "^0.6.5",
"@vitejs/plugin-react": "^4.3.1",
"@vitejs/plugin-react": "^4.3.2",
"babel-plugin-macros": "^3.1.0",
"jsdom": "^25.0.0",
"jsdom": "^25.0.1",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.6.2",
"vite": "^5.4.6",
"vite": "^5.4.8",
"vite-plugin-checker": "^0.8.0",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^2.1.1",
"vitest": "^2.1.2",
"vitest-mock-extended": "^2.0.2"
}
}

View File

@@ -6,16 +6,16 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.2.0</version>
<version>5.3.1</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
<properties>
<!-- renovate: datasource=node-version depName=node -->
<node.version>v20.17.0</node.version>
<node.version>v20.18.0</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>10.8.3</npm.version>
<npm.version>10.9.0</npm.version>
</properties>
<build>
@@ -23,7 +23,7 @@
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<version>1.15.1</version>
<?m2e ignore?>
<executions>
<execution>

View File

@@ -230,7 +230,7 @@ export const selectPreviousEntry = createAppAsyncThunk(
)
export const selectNextEntry = createAppAsyncThunk(
"entries/entry/selectNext",
(
async (
arg: {
expand: boolean
markAsRead: boolean
@@ -239,12 +239,20 @@ export const selectNextEntry = createAppAsyncThunk(
thunkApi
) => {
const state = thunkApi.getState()
const { entries } = state.entries
const { entries, hasMore, loading } = state.entries
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
if (nextIndex < entries.length) {
// load more entries if needed
// this can happen if the last entry is too large to fit on the screen and the infinite loader doesn't trigger
if (nextIndex >= entries.length && hasMore && !loading) {
await thunkApi.dispatch(loadMoreEntries())
}
const entriesAfterLoading = thunkApi.getState().entries.entries
if (nextIndex < entriesAfterLoading.length) {
thunkApi.dispatch(
selectEntry({
entry: entries[nextIndex],
entry: entriesAfterLoading[nextIndex],
expand: arg.expand,
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry,

View File

@@ -220,6 +220,7 @@ export interface ServerInfo {
websocketEnabled: boolean
websocketPingInterval: number
treeReloadInterval: number
forceRefreshCooldownDuration: number
}
export interface SharingSettings {
@@ -287,6 +288,7 @@ export interface UserModel {
created: number
lastLogin?: number
admin: boolean
lastForceRefresh?: number
}
export interface AdminSaveUserRequest {

View File

@@ -2,14 +2,10 @@ import { Trans } from "@lingui/macro"
import { Tooltip } from "@mantine/core"
import { Constants } from "app/constants"
import dayjs from "dayjs"
import { useEffect, useState } from "react"
import { useNow } from "hooks/useNow"
export function RelativeDate(props: { date: Date | number | undefined }) {
const [now, setNow] = useState(new Date())
useEffect(() => {
const interval = setInterval(() => setNow(new Date()), 60 * 1000)
return () => clearInterval(interval)
}, [])
const now = useNow(60 * 1000)
if (!props.date) return <Trans>N/A</Trans>
const date = dayjs(props.date)

View File

@@ -128,36 +128,28 @@ export function FeedEntries() {
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
useMousetrap("r", async () => await dispatch(reloadEntries()))
useMousetrap("j", async () => {
// load more entries if needed
// this can happen if the last entry is too large to fit on the screen and the infinite loader doesn't trigger
if (hasMore && !loading && selectedEntry === entries[entries.length - 1]) {
await dispatch(loadMoreEntries())
}
await dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
})
useMousetrap("n", async () => {
// load more entries if needed
// this can happen if the last entry is too large to fit on the screen and the infinite loader doesn't trigger
if (hasMore && !loading && selectedEntry === entries[entries.length - 1]) {
await dispatch(loadMoreEntries())
}
await dispatch(
selectNextEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
})
useMousetrap(
"j",
async () =>
await dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
)
useMousetrap(
"n",
async () =>
await dispatch(
selectNextEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
)
useMousetrap(
"k",
async () =>

View File

@@ -15,6 +15,9 @@ import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetr
import { useAppDispatch, useAppSelector } from "app/store"
import type { ViewMode } from "app/types"
import { setViewMode } from "app/user/slice"
import { reloadProfile } from "app/user/thunks"
import dayjs from "dayjs"
import { useNow } from "hooks/useNow"
import { type ReactNode, useState } from "react"
import {
TbChartLine,
@@ -92,12 +95,19 @@ const viewModeData: ViewModeControlItem[] = [
export function ProfileMenu(props: ProfileMenuProps) {
const [opened, setOpened] = useState(false)
const now = useNow()
const profile = useAppSelector(state => state.user.profile)
const admin = useAppSelector(state => state.user.profile?.admin)
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
const forceRefreshCooldownDuration = useAppSelector(state => state.server.serverInfos?.forceRefreshCooldownDuration)
const dispatch = useAppDispatch()
const { colorScheme, setColorScheme } = useMantineColorScheme()
const nextAvailableForceRefresh = profile?.lastForceRefresh
? profile.lastForceRefresh + (forceRefreshCooldownDuration ?? 0)
: now.getTime()
const forceRefreshEnabled = nextAvailableForceRefresh <= now.getTime()
const logout = () => {
window.location.href = "logout"
}
@@ -118,18 +128,32 @@ export function ProfileMenu(props: ProfileMenuProps) {
</Menu.Item>
<Menu.Item
leftSection={<TbWorldDownload size={iconSize} />}
onClick={async () =>
await client.feed.refreshAll().then(() => {
disabled={!forceRefreshEnabled}
onClick={async () => {
setOpened(false)
try {
await client.feed.refreshAll()
// reload profile to update last force refresh timestamp
await dispatch(reloadProfile())
showNotification({
message: <Trans>Your feeds have been queued for refresh.</Trans>,
color: "green",
autoClose: 1000,
})
setOpened(false)
})
}
} catch (error) {
showNotification({
message: <Trans>Force fetching feeds is not yet available.</Trans>,
color: "red",
autoClose: 2000,
})
}
}}
>
<Trans>Fetch all my feeds now</Trans>
{!forceRefreshEnabled && <span> ({dayjs.duration(nextAvailableForceRefresh - now.getTime()).format("HH:mm:ss")})</span>}
</Menu.Item>
<Divider />

View File

@@ -0,0 +1,10 @@
import { useEffect, useState } from "react"
export const useNow = (interval = 1000): Date => {
const [time, setTime] = useState(new Date())
useEffect(() => {
const t = setInterval(() => setTime(new Date()), interval)
return () => clearInterval(t)
}, [interval])
return time
}

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "تصفية التعبير"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "هل نسيت كلمة المرور؟"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Expressió de filtratge"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Heu oblidat la contrasenya?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtrování výrazu"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Zapomněli jste heslo?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Hidlo mynegiant"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Wedi anghofio cyfrinair?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtrerende udtryk"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glemt adgangskode?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filterausdruck"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Passwort vergessen?"

View File

@@ -376,6 +376,10 @@ msgstr "Fever API URL"
msgid "Filtering expression"
msgstr "Filtering expression"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr "Force fetching feeds is not yet available."
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Forgot password?"

View File

@@ -377,6 +377,10 @@ msgstr "URL de la API de Fever"
msgid "Filtering expression"
msgstr "Expresión de filtrado"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "¿Olvidaste la contraseña?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "بیان فیلتر"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "رمز عبور را فراموش کرده اید؟"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Suodattava lauseke"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Unohditko salasanan?"

View File

@@ -323,7 +323,7 @@ msgstr "Entrez votre mot de passe actuel pour changer les paramètres du profil"
#: src/components/settings/DisplaySettings.tsx
msgid "Entries to keep above the selected entry when scrolling"
msgstr ""
msgstr "Nombre d'entrées à conserver au-dessus de l'entrée sélectionnée lors d'un défilement"
#: src/components/settings/DisplaySettings.tsx
msgid "Entry headers"
@@ -376,6 +376,10 @@ msgstr "URL API Fever"
msgid "Filtering expression"
msgstr "Expression de filtrage"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr "La récupération forcée des flux n'est pas encore disponible."
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Mot de passe oublié ?"
@@ -614,7 +618,7 @@ msgstr "Sur mobile, afficher les boutons d'action en bas de l'écran"
#: src/components/settings/DisplaySettings.tsx
msgid "Only applies to compact, cozy and detailed modes"
msgstr ""
msgstr "Ne fonctionne que dans les modes Compact, Cozy, et Vue détaillée"
#: src/pages/ErrorPage.tsx
msgid "Oops!"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Expresión de filtrado"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Esqueceches o contrasinal?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Szűrő kifejezés"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Elfelejtette a jelszavát?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Memfilter ekspresi"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Lupa kata sandi?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Espressione filtrante"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Password dimenticata?"

View File

@@ -58,7 +58,7 @@ msgstr "ユーザー追加"
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Admin"
msgstr "管理"
msgstr "管理"
#: src/app/constants.ts
#: src/components/content/add/CategorySelect.tsx
@@ -323,7 +323,7 @@ msgstr "プロファイル設定を変更するには、現在のパスワード
#: src/components/settings/DisplaySettings.tsx
msgid "Entries to keep above the selected entry when scrolling"
msgstr ""
msgstr "エントリーを選択したとき、読みやすさに応じたスクロール調整を行います。"
#: src/components/settings/DisplaySettings.tsx
msgid "Entry headers"
@@ -376,6 +376,10 @@ msgstr "Fever API URL"
msgid "Filtering expression"
msgstr "フィルタリング式"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr "フィードの強制フェッチはまだ利用できません。"
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "パスワードをお忘れですか?"
@@ -398,7 +402,7 @@ msgstr "生成されたフィードURL"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr "Go to {0}"
msgstr "{0} に移動"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view"
@@ -418,11 +422,11 @@ msgstr "ID"
#: src/pages/app/FeedDetailsPage.tsx
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "空でない場合は、'true' または 'false' に評価される式。 'false' の場合、このフィードの新しいエントリは自動的に既読としてマークされます。"
msgstr "空でない場合は、'true' または 'false' に評価される式。 'false' の場合、このフィードの新しいエントリは自動的に既読としてマークされます。"
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr "エントリが画面に完全に収まらない場合"
msgstr "エントリが画面に完全に収まらない場合"
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
@@ -614,7 +618,7 @@ msgstr "モバイルでは、画面の下部にアクションボタンを表示
#: src/components/settings/DisplaySettings.tsx
msgid "Only applies to compact, cozy and detailed modes"
msgstr ""
msgstr "これはコンパクト/cozy/詳細モードでのみ適用されます"
#: src/pages/ErrorPage.tsx
msgid "Oops!"
@@ -751,7 +755,7 @@ msgstr "保存"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr "選択されたエントリーをページの上部にスクロールする"
msgstr "エントリーを選択したときのスクロール調整"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
@@ -774,11 +778,11 @@ msgstr "検索には少なくとも3文字が必要です"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "次のエントリーを開かずにフォーカスを設定する"
msgstr "次のエントリーを開かずにフォーカスする"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on previous entry without opening it"
msgstr "前のエントリーを開かずにフォーカスを設定する"
msgstr "前のエントリーを開かずにフォーカスする"
#: src/components/header/ProfileMenu.tsx
msgid "Settings"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "필터링 표현식"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "비밀번호를 잊으셨나요?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Ungkapan penapisan"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Lupa kata laluan?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtrerende uttrykk"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glemt passord?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Uitdrukking filteren"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Wachtwoord vergeten?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtrerende uttrykk"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glemt passord?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Wyrażenie filtrujące"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Zapomniałeś hasła?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtrando expressão"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Esqueceu a senha?"

View File

@@ -376,6 +376,10 @@ msgstr "Ссылка Fever API"
msgid "Filtering expression"
msgstr "Выражение фильтрации"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Забыли пароль?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtrovanie výrazu"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Zabudli ste heslo?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtrerande uttryck"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glömt lösenord?"

View File

@@ -376,6 +376,10 @@ msgstr ""
msgid "Filtering expression"
msgstr "Filtreleme ifadesi"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Parolanızı mı unuttunuz?"

View File

@@ -376,6 +376,10 @@ msgstr "Fever API 网址"
msgid "Filtering expression"
msgstr "过滤表达式"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "忘记密码?"

View File

@@ -6,11 +6,13 @@ import "react-contexify/ReactContexify.css"
import { App } from "App"
import { store } from "app/store"
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
import relativeTime from "dayjs/plugin/relativeTime"
import ReactDOM from "react-dom/client"
import { Provider } from "react-redux"
dayjs.extend(relativeTime)
dayjs.extend(duration)
const root = document.getElementById("root")
root &&

View File

@@ -8,7 +8,7 @@ h|[.header-title]##Configuration property##
h|Type
h|Default
a| [[commafeed-server_commafeed-hide-from-web-crawlers]] [.property-path]##`commafeed.hide-from-web-crawlers`##
a| [[commafeed-server_commafeed-hide-from-web-crawlers]] [.property-path]##link:#commafeed-server_commafeed-hide-from-web-crawlers[`commafeed.hide-from-web-crawlers`]##
[.description]
--
@@ -25,7 +25,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`true`
a| [[commafeed-server_commafeed-image-proxy-enabled]] [.property-path]##`commafeed.image-proxy-enabled`##
a| [[commafeed-server_commafeed-image-proxy-enabled]] [.property-path]##link:#commafeed-server_commafeed-image-proxy-enabled[`commafeed.image-proxy-enabled`]##
[.description]
--
@@ -42,7 +42,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`false`
a| [[commafeed-server_commafeed-password-recovery-enabled]] [.property-path]##`commafeed.password-recovery-enabled`##
a| [[commafeed-server_commafeed-password-recovery-enabled]] [.property-path]##link:#commafeed-server_commafeed-password-recovery-enabled[`commafeed.password-recovery-enabled`]##
[.description]
--
@@ -59,7 +59,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`false`
a| [[commafeed-server_commafeed-announcement]] [.property-path]##`commafeed.announcement`##
a| [[commafeed-server_commafeed-announcement]] [.property-path]##link:#commafeed-server_commafeed-announcement[`commafeed.announcement`]##
[.description]
--
@@ -76,7 +76,7 @@ endif::add-copy-button-to-env-var[]
|string
|
a| [[commafeed-server_commafeed-google-analytics-tracking-code]] [.property-path]##`commafeed.google-analytics-tracking-code`##
a| [[commafeed-server_commafeed-google-analytics-tracking-code]] [.property-path]##link:#commafeed-server_commafeed-google-analytics-tracking-code[`commafeed.google-analytics-tracking-code`]##
[.description]
--
@@ -93,7 +93,7 @@ endif::add-copy-button-to-env-var[]
|string
|
a| [[commafeed-server_commafeed-google-auth-key]] [.property-path]##`commafeed.google-auth-key`##
a| [[commafeed-server_commafeed-google-auth-key]] [.property-path]##link:#commafeed-server_commafeed-google-auth-key[`commafeed.google-auth-key`]##
[.description]
--
@@ -110,11 +110,11 @@ endif::add-copy-button-to-env-var[]
|string
|
h|[[commafeed-server_section_commafeed-http-client]] [.section-name.section-level0]##HTTP client configuration##
h|[[commafeed-server_section_commafeed-http-client]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-http-client[HTTP client configuration]##
h|Type
h|Default
a| [[commafeed-server_commafeed-http-client-user-agent]] [.property-path]##`commafeed.http-client.user-agent`##
a| [[commafeed-server_commafeed-http-client-user-agent]] [.property-path]##link:#commafeed-server_commafeed-http-client-user-agent[`commafeed.http-client.user-agent`]##
[.description]
--
@@ -131,7 +131,7 @@ endif::add-copy-button-to-env-var[]
|string
|
a| [[commafeed-server_commafeed-http-client-connect-timeout]] [.property-path]##`commafeed.http-client.connect-timeout`##
a| [[commafeed-server_commafeed-http-client-connect-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-connect-timeout[`commafeed.http-client.connect-timeout`]##
[.description]
--
@@ -148,7 +148,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`5S`
a| [[commafeed-server_commafeed-http-client-ssl-handshake-timeout]] [.property-path]##`commafeed.http-client.ssl-handshake-timeout`##
a| [[commafeed-server_commafeed-http-client-ssl-handshake-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-ssl-handshake-timeout[`commafeed.http-client.ssl-handshake-timeout`]##
[.description]
--
@@ -165,7 +165,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`5S`
a| [[commafeed-server_commafeed-http-client-socket-timeout]] [.property-path]##`commafeed.http-client.socket-timeout`##
a| [[commafeed-server_commafeed-http-client-socket-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-socket-timeout[`commafeed.http-client.socket-timeout`]##
[.description]
--
@@ -182,7 +182,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`10S`
a| [[commafeed-server_commafeed-http-client-response-timeout]] [.property-path]##`commafeed.http-client.response-timeout`##
a| [[commafeed-server_commafeed-http-client-response-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-response-timeout[`commafeed.http-client.response-timeout`]##
[.description]
--
@@ -199,7 +199,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`10S`
a| [[commafeed-server_commafeed-http-client-connection-time-to-live]] [.property-path]##`commafeed.http-client.connection-time-to-live`##
a| [[commafeed-server_commafeed-http-client-connection-time-to-live]] [.property-path]##link:#commafeed-server_commafeed-http-client-connection-time-to-live[`commafeed.http-client.connection-time-to-live`]##
[.description]
--
@@ -216,7 +216,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`30S`
a| [[commafeed-server_commafeed-http-client-idle-connections-eviction-interval]] [.property-path]##`commafeed.http-client.idle-connections-eviction-interval`##
a| [[commafeed-server_commafeed-http-client-idle-connections-eviction-interval]] [.property-path]##link:#commafeed-server_commafeed-http-client-idle-connections-eviction-interval[`commafeed.http-client.idle-connections-eviction-interval`]##
[.description]
--
@@ -233,7 +233,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`1M`
a| [[commafeed-server_commafeed-http-client-max-response-size]] [.property-path]##`commafeed.http-client.max-response-size`##
a| [[commafeed-server_commafeed-http-client-max-response-size]] [.property-path]##link:#commafeed-server_commafeed-http-client-max-response-size[`commafeed.http-client.max-response-size`]##
[.description]
--
@@ -250,11 +250,11 @@ endif::add-copy-button-to-env-var[]
|MemorySize link:#memory-size-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the MemorySize format]]
|`5M`
h|[[commafeed-server_section_commafeed-http-client-cache]] [.section-name.section-level1]##HTTP client cache configuration##
h|[[commafeed-server_section_commafeed-http-client-cache]] [.section-name.section-level1]##link:#commafeed-server_section_commafeed-http-client-cache[HTTP client cache configuration]##
h|Type
h|Default
a| [[commafeed-server_commafeed-http-client-cache-enabled]] [.property-path]##`commafeed.http-client.cache.enabled`##
a| [[commafeed-server_commafeed-http-client-cache-enabled]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-enabled[`commafeed.http-client.cache.enabled`]##
[.description]
--
@@ -271,7 +271,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`true`
a| [[commafeed-server_commafeed-http-client-cache-maximum-memory-size]] [.property-path]##`commafeed.http-client.cache.maximum-memory-size`##
a| [[commafeed-server_commafeed-http-client-cache-maximum-memory-size]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-maximum-memory-size[`commafeed.http-client.cache.maximum-memory-size`]##
[.description]
--
@@ -288,7 +288,7 @@ endif::add-copy-button-to-env-var[]
|MemorySize link:#memory-size-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the MemorySize format]]
|`10M`
a| [[commafeed-server_commafeed-http-client-cache-expiration]] [.property-path]##`commafeed.http-client.cache.expiration`##
a| [[commafeed-server_commafeed-http-client-cache-expiration]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-expiration[`commafeed.http-client.cache.expiration`]##
[.description]
--
@@ -307,11 +307,11 @@ endif::add-copy-button-to-env-var[]
h|[[commafeed-server_section_commafeed-feed-refresh]] [.section-name.section-level0]##Feed refresh engine settings##
h|[[commafeed-server_section_commafeed-feed-refresh]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-feed-refresh[Feed refresh engine settings]##
h|Type
h|Default
a| [[commafeed-server_commafeed-feed-refresh-interval]] [.property-path]##`commafeed.feed-refresh.interval`##
a| [[commafeed-server_commafeed-feed-refresh-interval]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-interval[`commafeed.feed-refresh.interval`]##
[.description]
--
@@ -328,7 +328,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`5M`
a| [[commafeed-server_commafeed-feed-refresh-interval-empirical]] [.property-path]##`commafeed.feed-refresh.interval-empirical`##
a| [[commafeed-server_commafeed-feed-refresh-interval-empirical]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-interval-empirical[`commafeed.feed-refresh.interval-empirical`]##
[.description]
--
@@ -345,7 +345,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`false`
a| [[commafeed-server_commafeed-feed-refresh-http-threads]] [.property-path]##`commafeed.feed-refresh.http-threads`##
a| [[commafeed-server_commafeed-feed-refresh-http-threads]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-http-threads[`commafeed.feed-refresh.http-threads`]##
[.description]
--
@@ -362,7 +362,7 @@ endif::add-copy-button-to-env-var[]
|int
|`3`
a| [[commafeed-server_commafeed-feed-refresh-database-threads]] [.property-path]##`commafeed.feed-refresh.database-threads`##
a| [[commafeed-server_commafeed-feed-refresh-database-threads]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-database-threads[`commafeed.feed-refresh.database-threads`]##
[.description]
--
@@ -379,7 +379,7 @@ endif::add-copy-button-to-env-var[]
|int
|`1`
a| [[commafeed-server_commafeed-feed-refresh-user-inactivity-period]] [.property-path]##`commafeed.feed-refresh.user-inactivity-period`##
a| [[commafeed-server_commafeed-feed-refresh-user-inactivity-period]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-user-inactivity-period[`commafeed.feed-refresh.user-inactivity-period`]##
[.description]
--
@@ -396,7 +396,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`0S`
a| [[commafeed-server_commafeed-feed-refresh-filtering-expression-evaluation-timeout]] [.property-path]##`commafeed.feed-refresh.filtering-expression-evaluation-timeout`##
a| [[commafeed-server_commafeed-feed-refresh-filtering-expression-evaluation-timeout]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-filtering-expression-evaluation-timeout[`commafeed.feed-refresh.filtering-expression-evaluation-timeout`]##
[.description]
--
@@ -413,12 +413,29 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`500MS`
a| [[commafeed-server_commafeed-feed-refresh-force-refresh-cooldown-duration]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-force-refresh-cooldown-duration[`commafeed.feed-refresh.force-refresh-cooldown-duration`]##
h|[[commafeed-server_section_commafeed-database]] [.section-name.section-level0]##Database settings##
[.description]
--
Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_FORCE_REFRESH_COOLDOWN_DURATION+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_FORCE_REFRESH_COOLDOWN_DURATION+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`0S`
h|[[commafeed-server_section_commafeed-database]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-database[Database settings]##
h|Type
h|Default
a| [[commafeed-server_commafeed-database-query-timeout]] [.property-path]##`commafeed.database.query-timeout`##
a| [[commafeed-server_commafeed-database-query-timeout]] [.property-path]##link:#commafeed-server_commafeed-database-query-timeout[`commafeed.database.query-timeout`]##
[.description]
--
@@ -435,11 +452,11 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`0S`
h|[[commafeed-server_section_commafeed-database-cleanup]] [.section-name.section-level1]##Database cleanup settings##
h|[[commafeed-server_section_commafeed-database-cleanup]] [.section-name.section-level1]##link:#commafeed-server_section_commafeed-database-cleanup[Database cleanup settings]##
h|Type
h|Default
a| [[commafeed-server_commafeed-database-cleanup-entries-max-age]] [.property-path]##`commafeed.database.cleanup.entries-max-age`##
a| [[commafeed-server_commafeed-database-cleanup-entries-max-age]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-entries-max-age[`commafeed.database.cleanup.entries-max-age`]##
[.description]
--
@@ -456,7 +473,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`365D`
a| [[commafeed-server_commafeed-database-cleanup-statuses-max-age]] [.property-path]##`commafeed.database.cleanup.statuses-max-age`##
a| [[commafeed-server_commafeed-database-cleanup-statuses-max-age]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-statuses-max-age[`commafeed.database.cleanup.statuses-max-age`]##
[.description]
--
@@ -473,7 +490,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`0S`
a| [[commafeed-server_commafeed-database-cleanup-max-feed-capacity]] [.property-path]##`commafeed.database.cleanup.max-feed-capacity`##
a| [[commafeed-server_commafeed-database-cleanup-max-feed-capacity]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-max-feed-capacity[`commafeed.database.cleanup.max-feed-capacity`]##
[.description]
--
@@ -490,7 +507,7 @@ endif::add-copy-button-to-env-var[]
|int
|`500`
a| [[commafeed-server_commafeed-database-cleanup-max-feeds-per-user]] [.property-path]##`commafeed.database.cleanup.max-feeds-per-user`##
a| [[commafeed-server_commafeed-database-cleanup-max-feeds-per-user]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-max-feeds-per-user[`commafeed.database.cleanup.max-feeds-per-user`]##
[.description]
--
@@ -507,7 +524,7 @@ endif::add-copy-button-to-env-var[]
|int
|`0`
a| [[commafeed-server_commafeed-database-cleanup-batch-size]] [.property-path]##`commafeed.database.cleanup.batch-size`##
a| [[commafeed-server_commafeed-database-cleanup-batch-size]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-batch-size[`commafeed.database.cleanup.batch-size`]##
[.description]
--
@@ -526,11 +543,11 @@ endif::add-copy-button-to-env-var[]
h|[[commafeed-server_section_commafeed-users]] [.section-name.section-level0]##Users settings##
h|[[commafeed-server_section_commafeed-users]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-users[Users settings]##
h|Type
h|Default
a| [[commafeed-server_commafeed-users-allow-registrations]] [.property-path]##`commafeed.users.allow-registrations`##
a| [[commafeed-server_commafeed-users-allow-registrations]] [.property-path]##link:#commafeed-server_commafeed-users-allow-registrations[`commafeed.users.allow-registrations`]##
[.description]
--
@@ -547,7 +564,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`false`
a| [[commafeed-server_commafeed-users-strict-password-policy]] [.property-path]##`commafeed.users.strict-password-policy`##
a| [[commafeed-server_commafeed-users-strict-password-policy]] [.property-path]##link:#commafeed-server_commafeed-users-strict-password-policy[`commafeed.users.strict-password-policy`]##
[.description]
--
@@ -564,7 +581,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`true`
a| [[commafeed-server_commafeed-users-create-demo-account]] [.property-path]##`commafeed.users.create-demo-account`##
a| [[commafeed-server_commafeed-users-create-demo-account]] [.property-path]##link:#commafeed-server_commafeed-users-create-demo-account[`commafeed.users.create-demo-account`]##
[.description]
--
@@ -582,11 +599,11 @@ endif::add-copy-button-to-env-var[]
|`false`
h|[[commafeed-server_section_commafeed-websocket]] [.section-name.section-level0]##Websocket settings##
h|[[commafeed-server_section_commafeed-websocket]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-websocket[Websocket settings]##
h|Type
h|Default
a| [[commafeed-server_commafeed-websocket-enabled]] [.property-path]##`commafeed.websocket.enabled`##
a| [[commafeed-server_commafeed-websocket-enabled]] [.property-path]##link:#commafeed-server_commafeed-websocket-enabled[`commafeed.websocket.enabled`]##
[.description]
--
@@ -603,7 +620,7 @@ endif::add-copy-button-to-env-var[]
|boolean
|`true`
a| [[commafeed-server_commafeed-websocket-ping-interval]] [.property-path]##`commafeed.websocket.ping-interval`##
a| [[commafeed-server_commafeed-websocket-ping-interval]] [.property-path]##link:#commafeed-server_commafeed-websocket-ping-interval[`commafeed.websocket.ping-interval`]##
[.description]
--
@@ -620,7 +637,7 @@ endif::add-copy-button-to-env-var[]
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`15M`
a| [[commafeed-server_commafeed-websocket-tree-reload-interval]] [.property-path]##`commafeed.websocket.tree-reload-interval`##
a| [[commafeed-server_commafeed-websocket-tree-reload-interval]] [.property-path]##link:#commafeed-server_commafeed-websocket-tree-reload-interval[`commafeed.websocket.tree-reload-interval`]##
[.description]
--

View File

@@ -6,16 +6,16 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.2.0</version>
<version>5.3.1</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
<properties>
<quarkus.version>3.14.4</quarkus.version>
<querydsl.version>6.7</querydsl.version>
<quarkus.version>3.15.1</quarkus.version>
<querydsl.version>6.8</querydsl.version>
<rome.version>2.1.0</rome.version>
<swagger.version>2.2.23</swagger.version>
<swagger.version>2.2.25</swagger.version>
<build.database>h2</build.database>
</properties>
@@ -238,7 +238,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.18.1</version>
<version>10.18.2</version>
</dependency>
</dependencies>
<executions>
@@ -294,7 +294,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>5.2.0</version>
<version>5.3.1</version>
</dependency>
<!-- compile-time processors -->
@@ -358,7 +358,7 @@
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId>
<version>4.2.27</version>
<version>4.2.28</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
@@ -450,7 +450,7 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
<version>5.4</version>
</dependency>
<!-- add brotli support for httpclient5 -->
<dependency>
@@ -461,7 +461,7 @@
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
<version>8.3.6</version>
<version>8.3.7</version>
</dependency>
<!-- test dependencies -->

View File

@@ -209,6 +209,12 @@ public interface CommaFeedConfiguration {
*/
@WithDefault("500ms")
Duration filteringExpressionEvaluationTimeout();
/**
* Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds.
*/
@WithDefault("0")
Duration forceRefreshCooldownDuration();
}
interface Database {

View File

@@ -18,7 +18,8 @@ public class JacksonCustomizer implements ObjectMapperCustomizer {
objectMapper.registerModule(new JavaTimeModule());
// read and write instants as milliseconds instead of nanoseconds
objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
// add support for serializing metrics

View File

@@ -126,7 +126,13 @@ public class HttpGetter {
log.debug("fetching {}", request.getUrl());
HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build());
context.setRequestConfig(RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
// causes issues with some feeds
// see https://github.com/Athou/commafeed/issues/1572
// and https://issues.apache.org/jira/browse/HTTPCLIENT-2344
.setProtocolUpgradeEnabled(false)
.build());
return client.execute(request.toClassicHttpRequest(), context, resp -> {
byte[] content = resp.getEntity() == null ? null
@@ -176,7 +182,7 @@ public class HttpGetter {
int poolSize = config.feedRefresh().httpThreads();
return PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(Apache5SslUtils.toSocketFactory(sslFactory))
.setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory))
.setDefaultConnectionConfig(ConnectionConfig.custom()
.setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
.setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))

View File

@@ -52,4 +52,7 @@ public class User extends AbstractModel {
@Column
private Instant recoverPasswordTokenDate;
@Column
private Instant lastForceRefresh;
}

View File

@@ -98,12 +98,19 @@ public class FeedSubscriptionService {
}
}
public void refreshAll(User user) {
public void refreshAll(User user) throws ForceFeedRefreshTooSoonException {
Instant lastForceRefresh = user.getLastForceRefresh();
if (lastForceRefresh != null && lastForceRefresh.plus(config.feedRefresh().forceRefreshCooldownDuration()).isAfter(Instant.now())) {
throw new ForceFeedRefreshTooSoonException();
}
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
Feed feed = sub.getFeed();
feedRefreshEngine.refreshImmediately(feed);
}
user.setLastForceRefresh(Instant.now());
}
public void refreshAllUpForRefresh(User user) {
@@ -130,4 +137,11 @@ public class FeedSubscriptionService {
}
}
@SuppressWarnings("serial")
public static class ForceFeedRefreshTooSoonException extends Exception {
private ForceFeedRefreshTooSoonException() {
super();
}
}
}

View File

@@ -43,4 +43,7 @@ public class ServerInfo implements Serializable {
@Schema(requiredMode = RequiredMode.REQUIRED)
private long treeReloadInterval;
@Schema(requiredMode = RequiredMode.REQUIRED)
private long forceRefreshCooldownDuration;
}

View File

@@ -41,4 +41,7 @@ public class UserModel implements Serializable {
@Schema(description = "user is admin", requiredMode = RequiredMode.REQUIRED)
private boolean admin;
@Schema(description = "user last force refresh", type = "number")
private Instant lastForceRefresh;
}

View File

@@ -10,6 +10,7 @@ import java.util.Objects;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.hc.core5.http.HttpStatus;
import org.jboss.resteasy.reactive.Cache;
import org.jboss.resteasy.reactive.RestForm;
@@ -40,6 +41,7 @@ import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterEx
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
import com.commafeed.backend.service.FeedSubscriptionService;
import com.commafeed.backend.service.FeedSubscriptionService.ForceFeedRefreshTooSoonException;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.FeedInfo;
@@ -276,8 +278,13 @@ public class FeedREST {
@Operation(summary = "Queue all feeds of the user for refresh", description = "Manually add all feeds of the user to the refresh queue")
public Response queueAllForRefresh() {
User user = authenticationContext.getCurrentUser();
feedSubscriptionService.refreshAll(user);
return Response.ok().build();
try {
feedSubscriptionService.refreshAll(user);
return Response.ok().build();
} catch (ForceFeedRefreshTooSoonException e) {
return Response.status(HttpStatus.SC_TOO_MANY_REQUESTS).build();
}
}
@Path("/refresh")

View File

@@ -61,6 +61,7 @@ public class ServerREST {
infos.setWebsocketEnabled(config.websocket().enabled());
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis());
infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis());
return Response.ok(infos).build();
}

View File

@@ -212,6 +212,7 @@ public class UserREST {
userModel.setEmail(user.getEmail());
userModel.setEnabled(!user.isDisabled());
userModel.setApiKey(user.getApiKey());
userModel.setLastForceRefresh(user.getLastForceRefresh());
for (UserRole role : userRoleDAO.findAll(user)) {
if (role.getRole() == Role.ADMIN) {
userModel.setAdmin(true);

View File

@@ -49,6 +49,7 @@ quarkus.native.add-all-charsets=true
%test.commafeed.users.allow-registrations=true
%test.commafeed.password-recovery-enabled=true
%test.commafeed.http-client.cache.enabled=false
%test.commafeed.feed-refresh.force-refresh-cooldown-duration=1m
# prod profile overrides

View File

@@ -0,0 +1,11 @@
<?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="lastForceRefresh" author="athou">
<addColumn tableName="USERS">
<column name="lastForceRefresh" type="${timestamp_type}" />
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -33,5 +33,6 @@
<include file="changelogs/db.changelog-4.4.xml" />
<include file="changelogs/db.changelog-5.1.xml" />
<include file="changelogs/db.changelog-5.2.xml" />
<include file="changelogs/db.changelog-5.3.xml" />
</databaseChangeLog>

View File

@@ -239,6 +239,24 @@ class HttpGetterTest {
Assertions.assertEquals("ok", new String(result.getContent()));
}
@Test
void doesNotUseUpgradeProtocolHeader() {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
calls.incrementAndGet();
if (req.containsHeader(HttpHeaders.UPGRADE)) {
throw new Exception("upgrade header should not be sent by the client");
}
return HttpResponse.response().withBody("ok");
});
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
Assertions.assertEquals(1, calls.get());
}
@Nested
class Compression {

View File

@@ -125,7 +125,7 @@ public abstract class BaseIT {
.as(Entries.class);
}
protected void forceRefreshAllFeeds() {
RestAssured.given().get("rest/feed/refreshAll").then().statusCode(HttpStatus.SC_OK);
protected int forceRefreshAllFeeds() {
return RestAssured.given().get("rest/feed/refreshAll").then().extract().statusCode();
}
}

View File

@@ -183,11 +183,13 @@ class FeedIT extends BaseIT {
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
forceRefreshAllFeeds();
Assertions.assertEquals(HttpStatus.SC_OK, forceRefreshAllFeeds());
Awaitility.await()
.atMost(Duration.ofSeconds(15))
.until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold));
Assertions.assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, forceRefreshAllFeeds());
}
}

View File

@@ -21,6 +21,7 @@ class ServerIT extends BaseIT {
Assertions.assertTrue(serverInfos.isWebsocketEnabled());
Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval());
Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval());
Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration());
}
}

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.2.0</version>
<version>5.3.1</version>
<name>CommaFeed</name>
<packaging>pom</packaging>