Compare commits

..

90 Commits
5.0.0 ... 5.1.0

Author SHA1 Message Date
Athou
643c969faf release 5.1.0 2024-09-02 17:47:08 +02:00
renovate[bot]
85f9469d6d Update linguijs monorepo to ^4.11.4 2024-09-02 14:07:26 +00:00
renovate[bot]
0df0d50695 Lock file maintenance 2024-09-02 01:33:37 +00:00
Athou
5e9256c197 reduce intermediate database cleanup logging level to reduce logging volume 2024-09-01 21:51:07 +02:00
Athou
b67e1a92f5 fix hibernate warnings about wrong types 2024-09-01 18:21:26 +02:00
Athou
d250e4bc26 add missing foreign key on feedentrystatuses.user_id 2024-09-01 18:02:38 +02:00
Athou
dcf1f41f2d add config doc sections 2024-09-01 13:47:09 +02:00
renovate[bot]
3df6ba1457 Update dependency axios to ^1.7.7 2024-08-31 22:33:06 +00:00
renovate[bot]
b89928f6c6 Update dependency axios to ^1.7.6 2024-08-30 21:06:14 +00:00
renovate[bot]
2e014484e3 Update mantine monorepo to ^7.12.2 2024-08-30 18:47:41 +00:00
renovate[bot]
3b2b18fd2e Update dependency com.puppycrawl.tools:checkstyle to v10.18.1 2024-08-30 15:35:27 +00:00
renovate[bot]
ebf1e592ff Update dependency @types/react to ^18.3.5 2024-08-30 10:40:03 +00:00
renovate[bot]
88404b91d8 Update dependency npm to v10.8.3 2024-08-28 20:30:31 +00:00
Athou
9cbb60313c there are only native implementations of brotli encoders, don't use them because it doesn't work on all platforms 2024-08-28 20:53:48 +02:00
Jérémie Panzer
b95d417f5e Merge pull request #1526 from Athou/renovate/quarkus.version
Update quarkus.version to v3.14.1 (minor)
2024-08-28 19:27:02 +02:00
Athou
994f1fb121 quarkus-config-doc-maven-plugin is now required to generate the documentation 2024-08-28 19:08:34 +02:00
renovate[bot]
e533c1fa4b Update quarkus.version to v3.14.1 2024-08-28 16:31:58 +00:00
renovate[bot]
d0d946ffe9 Update dependency vitest-mock-extended to ^2.0.2 2024-08-28 15:39:32 +00:00
Athou
e3bcc824c7 use a property to sync swagger versions 2024-08-28 17:36:15 +02:00
Athou
357e4e207f add a test to make sure brotli compression is supported 2024-08-28 17:34:24 +02:00
Athou
2aee961600 specify what version of checkstyle we want to use 2024-08-28 09:20:18 +02:00
Athou
3aa1987319 make renovate ignore all "io.quarkus" versions 2024-08-28 09:10:03 +02:00
Jérémie Panzer
ae15f61fc2 Merge pull request #1535 from Athou/renovate/org.apache.maven.plugins-maven-failsafe-plugin-3.x
Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.0
2024-08-27 16:55:50 +02:00
Jérémie Panzer
e58f92a812 Merge pull request #1536 from Athou/renovate/org.apache.maven.plugins-maven-surefire-plugin-3.x
Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.0
2024-08-27 16:55:38 +02:00
renovate[bot]
46383924b1 Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.0 2024-08-27 14:39:46 +00:00
renovate[bot]
071920e864 Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.0 2024-08-27 14:39:42 +00:00
Athou
012238e6a9 Docker README tweak 2024-08-27 13:40:18 +02:00
Athou
a565566c50 remove deprecation warning 2024-08-27 09:02:42 +02:00
Athou
550804c666 use @Transactional where possible 2024-08-27 08:47:39 +02:00
Athou
f7a4a33f5e fix wrong path in documentation 2024-08-27 08:41:53 +02:00
Athou
f1b19ebae3 after reading the spec, what we want is actually "no-cache" that actually means "cache but revalidate immediately" using If-Modified-Since request headers and 304 response codes 2024-08-26 11:41:56 +02:00
Jérémie Panzer
4049fa2e17 Merge pull request #1534 from canoine/master
Update fr/messages.po
2024-08-26 09:43:21 +02:00
canoine
28808cf4f5 Merge pull request #2 from canoine/canoine-patch-1
Update fr/messages.po
2024-08-26 09:07:18 +02:00
canoine
870b46cf9d Update fr/messages.po
Blind update, as I didn't find where some of the new fields are shown.
2024-08-26 08:58:03 +02:00
renovate[bot]
9c20dea99c Lock file maintenance 2024-08-26 02:13:39 +00:00
Athou
63c7679067 make sure the webapp and openapi documentation are always up to date by preventing caching 2024-08-26 00:28:44 +02:00
Athou
764c1a6430 add setting for showing unread count in tab/favicon (#1518) 2024-08-25 20:24:57 +02:00
renovate[bot]
bb6578bdd0 Update dependency org.passay:passay to v1.6.5 2024-08-25 03:23:37 +00:00
renovate[bot]
748c8531ad Lock file maintenance 2024-08-23 19:32:29 +00:00
Athou
a734fe68d2 fix link README 2024-08-23 21:21:15 +02:00
renovate[bot]
cc5ebc55a4 Update dependency axios to ^1.7.5 2024-08-23 14:17:51 +00:00
Jérémie Panzer
aa396c1e1c Merge pull request #1531 from Athou/renovate/monaco-editor-0.x
Update dependency monaco-editor to ^0.51.0
2024-08-23 11:51:22 +02:00
Athou
fbf87ff291 make renovate ignore quarkus-extension-processor 2024-08-23 11:44:24 +02:00
renovate[bot]
e9f3ffddf4 Update dependency monaco-editor to ^0.51.0 2024-08-23 09:33:00 +00:00
Jérémie Panzer
695518d68b Merge pull request #1530 from Athou/renovate/querydsl.version
Update querydsl.version to v6.7 (minor)
2024-08-23 05:34:54 +02:00
renovate[bot]
5d96c1e12b Update querydsl.version to v6.7 2024-08-22 22:39:22 +00:00
Jérémie Panzer
3a72a1cc04 Merge pull request #1529 from Athou/renovate/org.apache.maven.plugins-maven-checkstyle-plugin-3.x
Update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.5.0
2024-08-22 21:15:42 +02:00
renovate[bot]
54f5714108 Update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.5.0 2024-08-22 18:58:15 +00:00
Jérémie Panzer
04811c7eca Merge pull request #1528 from Athou/renovate/org.apache.maven.plugins-maven-help-plugin-3.x
Update dependency org.apache.maven.plugins:maven-help-plugin to v3.5.0
2024-08-22 07:10:46 +02:00
renovate[bot]
6856736ddb Update dependency org.apache.maven.plugins:maven-help-plugin to v3.5.0 2024-08-21 21:04:31 +00:00
Athou
db6c43993a simpler workaround for cookie max-age 2024-08-21 21:56:57 +02:00
Jérémie Panzer
508a22576a Merge pull request #1527 from Athou/renovate/node-20.x
Update dependency node to v20.17.0
2024-08-21 21:35:26 +02:00
renovate[bot]
8fb012b3a1 Update dependency node to v20.17.0 2024-08-21 18:25:57 +00:00
renovate[bot]
133781d314 Update dependency @emotion/react to ^11.13.3 2024-08-21 11:41:31 +00:00
renovate[bot]
50cb12896e Update dependency vite to ^5.4.2 2024-08-21 02:14:50 +00:00
renovate[bot]
79a4315941 Update dependency @types/react to ^18.3.4 2024-08-20 22:13:31 +00:00
renovate[bot]
33a2f76521 Update dependency dayjs to ^1.11.13 2024-08-20 19:25:48 +00:00
Jérémie Panzer
d4041a1d88 Merge pull request #1525 from Athou/renovate/patch-quarkus.version
Update quarkus.version to v3.13.3 (patch)
2024-08-20 21:24:48 +02:00
renovate[bot]
09f2f56446 Update quarkus.version to v3.13.3 2024-08-20 17:16:54 +00:00
Athou
a0c3eda506 remove warning 2024-08-20 10:03:15 +02:00
Athou
84de3199cc guava version is managed by quarkus 2024-08-20 10:01:13 +02:00
Athou
a7e8309d63 fix docker readme missing word 2024-08-20 08:36:31 +02:00
Athou
7e74d2f6f4 release 5.0.2 2024-08-20 07:27:46 +02:00
Athou
dc25d53dc0 github actions is sometimes slow, increase timeout for tests 2024-08-20 00:03:45 +02:00
Athou
ac1a927836 remove google-api-services-youtube because it doesn't play nicely with native-image 2024-08-19 23:55:38 +02:00
Athou
b50b69adb2 Revert "better workaround for cookie max-age" because it causes issues with favicon fetching 2024-08-19 18:27:50 +02:00
Athou
b112e912af release 5.0.1 2024-08-19 15:55:13 +02:00
Athou
ece55727d3 better workaround for cookie max-age 2024-08-19 14:31:12 +02:00
Athou
181dd24b57 format both Dockerfiles the same way 2024-08-19 10:29:35 +02:00
Athou
10008ca0e8 add link to all quarkus settings 2024-08-19 09:38:26 +02:00
Athou
134c4621a8 docker README tweaks 2024-08-19 08:47:23 +02:00
Athou
51f15bf487 github actions is sometimes very slow, increase default timeouts for tests 2024-08-19 08:26:06 +02:00
renovate[bot]
49ae2c88ad Lock file maintenance 2024-08-19 05:53:45 +00:00
Jérémie Panzer
43a628fc55 Merge pull request #1523 from Athou/renovate/org.apache.maven.plugins-maven-surefire-plugin-3.x
Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.4.0
2024-08-19 07:51:49 +02:00
Jérémie Panzer
7f71f95f7c Merge pull request #1522 from Athou/renovate/org.apache.maven.plugins-maven-failsafe-plugin-3.x
Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.4.0
2024-08-19 07:51:41 +02:00
Athou
e8d5eab419 compile to native image with support for older CPUs 2024-08-19 07:08:18 +02:00
renovate[bot]
de3a6b1f20 Update dependency io.dropwizard.metrics:metrics-json to v4.2.27 2024-08-19 00:25:33 +00:00
renovate[bot]
849742e19a Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.4.0 2024-08-18 21:04:13 +00:00
renovate[bot]
b6392b114c Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.4.0 2024-08-18 21:04:10 +00:00
Athou
4db0c775ff add settings documentation 2024-08-18 21:23:42 +02:00
Athou
ff9374f1ed README tweak 2024-08-18 19:20:34 +02:00
Athou
ea86c9bb1f README tweak 2024-08-18 17:07:31 +02:00
Athou
e6dd088abe fix typo 2024-08-18 17:00:59 +02:00
Jérémie Panzer
c039d8f3a4 Merge pull request #1521 from Athou/renovate/com.google.apis-google-api-services-youtube-3.0.x
Update dependency com.google.apis:google-api-services-youtube to v3-rev20240814-2.0.0
2024-08-18 14:51:39 +02:00
renovate[bot]
bffa6329fd Update dependency com.google.apis:google-api-services-youtube to v3-rev20240814-2.0.0 2024-08-18 10:58:38 +00:00
Athou
b88e5d2847 increase ssl handshake timeout during tests to fix build on slower machines 2024-08-18 12:25:52 +02:00
Jérémie Panzer
0fc4fcd406 Merge pull request #1516 from Athou/renovate/react-icons-5.x
Update dependency react-icons to ^5.3.0
2024-08-18 10:39:10 +02:00
Athou
f04ca21394 Twitter has been renamed X 2024-08-18 10:15:17 +02:00
renovate[bot]
a82fca130f Update dependency react-icons to ^5.3.0 2024-08-18 07:58:41 +00:00
Athou
70d494798c Docker readme tweaks 2024-08-18 09:45:44 +02:00
71 changed files with 1726 additions and 553 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## [5.1.0]
- Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518)
- Fixed an issue that could prevent the app from starting on some systems (#1532)
- Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to date
- Reduced database cleanup log verbosity
## [5.0.2]
- Fix favicon fetching for Youtube channels in native mode when Google auth key is set
- Fix an error that appears in the logs when fetching some favicons
## [5.0.1]
- Configure native compilation to support older CPU architectures (#1524)
## [5.0.0] ## [5.0.0]
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in

View File

@@ -80,7 +80,7 @@ the `data` directory of the current directory.
To use a different database, you will need to configure the following properties: To use a different database, you will need to configure the following properties:
- `quarkus.datasource.jdbc-url` - `quarkus.datasource.jdbc.url`
- e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE` - e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE`
- e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed` - e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed`
- e.g. for MySQL: - e.g. for MySQL:
@@ -92,19 +92,20 @@ To use a different database, you will need to configure the following properties
There are multiple ways to configure CommaFeed: There are multiple ways to configure CommaFeed:
- a [properties](https://en.wikipedia.org/wiki/.properties) file in `config/application.properties` (keys in kebab-case) - a `config/application.properties` [properties](https://en.wikipedia.org/wiki/.properties) file relative to the working
directory (keys in kebab-case)
- Command line arguments prefixed with `-D` (keys in kebab-case) - Command line arguments prefixed with `-D` (keys in kebab-case)
- Environment variables (keys in UPPER_CASE) - Environment variables (keys in UPPER_CASE)
- an .env file in the working directory (keys in UPPER_CASE) - a `.env` file in the working directory (keys in UPPER_CASE)
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos. The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
All [CommaFeed settings](commafeed-server/doc/commafeed.adoc) are optional and have sensible default values.
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup, When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters). `quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters).
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
All [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java)
are optional and have sensible default values.
When started, the server will listen on http://localhost:8082. When started, the server will listen on http://localhost:8082.
The default user is `admin` and the default password is `admin`. The default user is `admin` and the default password is `admin`.
@@ -139,7 +140,8 @@ slightly slower throughput.
IBM provides precompiled binaries for OpenJ9 IBM provides precompiled binaries for OpenJ9
named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/). named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/).
This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/blob/master/Dockerfile). This is the JVM used in
the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm).
## Translation ## Translation

File diff suppressed because it is too large Load Diff

View File

@@ -15,24 +15,24 @@
"i18n:extract": "lingui extract --clean" "i18n:extract": "lingui extract --clean"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.3",
"@fontsource/open-sans": "^5.0.29", "@fontsource/open-sans": "^5.0.29",
"@lingui/core": "^4.11.3", "@lingui/core": "^4.11.4",
"@lingui/macro": "^4.11.3", "@lingui/macro": "^4.11.4",
"@lingui/react": "^4.11.3", "@lingui/react": "^4.11.4",
"@mantine/core": "^7.12.1", "@mantine/core": "^7.12.2",
"@mantine/form": "^7.12.1", "@mantine/form": "^7.12.2",
"@mantine/hooks": "^7.12.1", "@mantine/hooks": "^7.12.2",
"@mantine/modals": "^7.12.1", "@mantine/modals": "^7.12.2",
"@mantine/notifications": "^7.12.1", "@mantine/notifications": "^7.12.2",
"@mantine/spotlight": "^7.12.1", "@mantine/spotlight": "^7.12.2",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"axios": "^1.7.4", "axios": "^1.7.7",
"dayjs": "^1.11.12", "dayjs": "^1.11.13",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"monaco-editor": "^0.50.0", "monaco-editor": "^0.51.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"react": "^18.3.1", "react": "^18.3.1",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
@@ -42,7 +42,7 @@
"react-draggable": "^4.4.6", "react-draggable": "^4.4.6",
"react-ga4": "^2.1.0", "react-ga4": "^2.1.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-icons": "^5.2.1", "react-icons": "^5.3.0",
"react-infinite-scroller": "^1.2.6", "react-infinite-scroller": "^1.2.6",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router-dom": "^6.26.1", "react-router-dom": "^6.26.1",
@@ -57,10 +57,10 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.8.3", "@biomejs/biome": "^1.8.3",
"@lingui/cli": "^4.11.3", "@lingui/cli": "^4.11.4",
"@lingui/vite-plugin": "^4.11.3", "@lingui/vite-plugin": "^4.11.4",
"@types/mousetrap": "^1.6.15", "@types/mousetrap": "^1.6.15",
"@types/react": "^18.3.3", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
"@types/react-infinite-scroller": "^1.2.5", "@types/react-infinite-scroller": "^1.2.5",
@@ -71,9 +71,9 @@
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.1", "vite": "^5.4.2",
"vite-tsconfig-paths": "^5.0.1", "vite-tsconfig-paths": "^5.0.1",
"vitest": "^2.0.5", "vitest": "^2.0.5",
"vitest-mock-extended": "^2.0.0" "vitest-mock-extended": "^2.0.2"
} }
} }

View File

@@ -6,16 +6,16 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>5.0.0</version> <version>5.1.0</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>
<properties> <properties>
<!-- renovate: datasource=node-version depName=node --> <!-- renovate: datasource=node-version depName=node -->
<node.version>v20.16.0</node.version> <node.version>v20.17.0</node.version>
<!-- renovate: datasource=npm depName=npm --> <!-- renovate: datasource=npm depName=npm -->
<npm.version>10.8.2</npm.version> <npm.version>10.8.3</npm.version>
</properties> </properties>
<build> <build>

View File

@@ -71,7 +71,7 @@ function Providers(props: { children: React.ReactNode }) {
) )
} }
// swagger-ui is very large, load only on-demand // api documentation page is very large, load only on-demand
const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage")) const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage"))
function AppRoutes() { function AppRoutes() {
@@ -142,16 +142,18 @@ function GoogleAnalyticsHandler() {
return null return null
} }
function FaviconHandler() { function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
const root = useAppSelector(state => state.tree.rootCategory) return <Helmet title={enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"} />
}
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
useEffect(() => { useEffect(() => {
const unreadCount = categoryUnreadCount(root) if (enabled && unreadCount > 0) {
if (unreadCount === 0) {
Tinycon.reset()
} else {
Tinycon.setBubble(unreadCount) Tinycon.setBubble(unreadCount)
} else {
Tinycon.reset()
} }
}, [root]) }, [unreadCount, enabled])
return null return null
} }
@@ -179,8 +181,13 @@ function CustomCode() {
export function App() { export function App() {
useI18n() useI18n()
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const unreadCount = categoryUnreadCount(root)
useEffect(() => { useEffect(() => {
dispatch(reloadServerInfos()) dispatch(reloadServerInfos())
}, [dispatch]) }, [dispatch])
@@ -188,7 +195,8 @@ export function App() {
return ( return (
<Providers> <Providers>
<> <>
<FaviconHandler /> <UnreadCountTitleHandler unreadCount={unreadCount} enabled={unreadCountTitle} />
<UnreadCountFaviconHandler unreadCount={unreadCount} enabled={unreadCountFavicon} />
<BrowserExtensionBadgeUnreadCountHandler /> <BrowserExtensionBadgeUnreadCountHandler />
<HashRouter> <HashRouter>
<GoogleAnalyticsHandler /> <GoogleAnalyticsHandler />

View File

@@ -1,7 +1,7 @@
import { t } from "@lingui/macro" import { t } from "@lingui/macro"
import type { IconType } from "react-icons" import type { IconType } from "react-icons"
import { FaAt } from "react-icons/fa" import { FaAt } from "react-icons/fa"
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si" import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
import type { Category, Entry, SharingSettings } from "./types" import type { Category, Entry, SharingSettings } from "./types"
const categories: Record<string, Category> = { const categories: Record<string, Category> = {
@@ -50,10 +50,10 @@ const sharing: {
url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`, url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
}, },
twitter: { twitter: {
label: "Twitter", label: "X",
icon: SiTwitter, icon: SiX,
color: "#1D9BF0", color: "#000000",
url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`, url: (url, desc) => `https://x.com/share?text=${desc}&url=${url}`,
}, },
tumblr: { tumblr: {
label: "Tumblr", label: "Tumblr",

View File

@@ -248,6 +248,8 @@ export interface Settings {
markAllAsReadConfirmation: boolean markAllAsReadConfirmation: boolean
customContextMenu: boolean customContextMenu: boolean
mobileFooter: boolean mobileFooter: boolean
unreadCountTitle: boolean
unreadCountFavicon: boolean
sharingSettings: SharingSettings sharingSettings: SharingSettings
} }

View File

@@ -16,6 +16,8 @@ import {
changeSharingSetting, changeSharingSetting,
changeShowRead, changeShowRead,
changeStarIconDisplayMode, changeStarIconDisplayMode,
changeUnreadCountFavicon,
changeUnreadCountTitle,
reloadProfile, reloadProfile,
reloadSettings, reloadSettings,
reloadTags, reloadTags,
@@ -91,6 +93,14 @@ export const userSlice = createSlice({
if (!state.settings) return if (!state.settings) return
state.settings.mobileFooter = action.meta.arg state.settings.mobileFooter = action.meta.arg
}) })
builder.addCase(changeUnreadCountTitle.pending, (state, action) => {
if (!state.settings) return
state.settings.unreadCountTitle = action.meta.arg
})
builder.addCase(changeUnreadCountFavicon.pending, (state, action) => {
if (!state.settings) return
state.settings.unreadCountFavicon = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => { builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
@@ -107,6 +117,8 @@ export const userSlice = createSlice({
changeMarkAllAsReadConfirmation.fulfilled, changeMarkAllAsReadConfirmation.fulfilled,
changeCustomContextMenu.fulfilled, changeCustomContextMenu.fulfilled,
changeMobileFooter.fulfilled, changeMobileFooter.fulfilled,
changeUnreadCountTitle.fulfilled,
changeUnreadCountFavicon.fulfilled,
changeSharingSetting.fulfilled changeSharingSetting.fulfilled
), ),
() => { () => {

View File

@@ -77,6 +77,16 @@ export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (
if (!settings) return if (!settings) return
client.user.saveSettings({ ...settings, mobileFooter }) client.user.saveSettings({ ...settings, mobileFooter })
}) })
export const changeUnreadCountTitle = createAppAsyncThunk("settings/unreadCountTitle", (unreadCountTitle: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, unreadCountTitle })
})
export const changeUnreadCountFavicon = createAppAsyncThunk("settings/unreadCountFavicon", (unreadCountFavicon: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, unreadCountFavicon })
})
export const changeSharingSetting = createAppAsyncThunk( export const changeSharingSetting = createAppAsyncThunk(
"settings/sharingSetting", "settings/sharingSetting",
( (

View File

@@ -17,6 +17,8 @@ import {
changeSharingSetting, changeSharingSetting,
changeShowRead, changeShowRead,
changeStarIconDisplayMode, changeStarIconDisplayMode,
changeUnreadCountFavicon,
changeUnreadCountTitle,
} from "app/user/thunks" } from "app/user/thunks"
import { locales } from "i18n" import { locales } from "i18n"
import type { ReactNode } from "react" import type { ReactNode } from "react"
@@ -32,6 +34,8 @@ export function DisplaySettings() {
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation) const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu) const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter) const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { _ } = useLingui() const { _ } = useLingui()
@@ -91,6 +95,20 @@ export function DisplaySettings() {
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))} onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
/> />
<Divider label={<Trans>Browser tab</Trans>} labelPosition="center" />
<Switch
label={<Trans>Show unread count in tab title</Trans>}
checked={unreadCountTitle}
onChange={async e => await dispatch(changeUnreadCountTitle(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show unread count in tab favicon</Trans>}
checked={unreadCountFavicon}
onChange={async e => await dispatch(changeUnreadCountFavicon(e.currentTarget.checked))}
/>
<Divider label={<Trans>Entry headers</Trans>} labelPosition="center" /> <Divider label={<Trans>Entry headers</Trans>} labelPosition="center" />
<Select <Select

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr "Extensió del navegador necessària per a Chrome"
msgid "Browser extention" msgid "Browser extention"
msgstr "Extensió del navegador" msgstr "Extensió del navegador"
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr "Mostra el menú natiu (escriptori)"
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr "Browser-Erweiterung für Chrome benötigt"
msgid "Browser extention" msgid "Browser extention"
msgstr "Browser-Erweiterung" msgstr "Browser-Erweiterung"
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr "Natives Menü anzeigen (Desktop)"
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr "Browser extension required for Chrome"
msgid "Browser extention" msgid "Browser extention"
msgstr "Browser extention" msgstr "Browser extention"
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr "Browser tab"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr "Show native menu (desktop)"
msgid "Show star icon" msgid "Show star icon"
msgstr "Show star icon" msgstr "Show star icon"
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr "Show unread count in tab favicon"
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr "Show unread count in tab title"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr "L'extension navigateur est nécessaire sur Chrome"
msgid "Browser extention" msgid "Browser extention"
msgstr "Extension navigateur" msgstr "Extension navigateur"
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr "Onglet navigateur"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -178,7 +182,7 @@ msgstr "Fermer le menu"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd" msgid "Cmd"
msgstr "" msgstr "Cmd"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
@@ -319,7 +323,7 @@ msgstr "Entrez votre mot de passe actuel pour changer les paramètres du profil"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Entry headers" msgid "Entry headers"
msgstr "" msgstr "En-têtes de l'entrée"
#: src/components/Alert.tsx #: src/components/Alert.tsx
msgid "Error" msgid "Error"
@@ -582,7 +586,7 @@ msgstr "Fin de la liste"
#: src/components/content/ShareButtons.tsx #: src/components/content/ShareButtons.tsx
msgid "No sharing options available." msgid "No sharing options available."
msgstr "" msgstr "Aucune option de partage disponible"
#: src/components/sidebar/TreeSearch.tsx #: src/components/sidebar/TreeSearch.tsx
msgid "Nothing found" msgid "Nothing found"
@@ -594,11 +598,11 @@ msgstr "Du plus ancien au plus récent"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "On desktop" msgid "On desktop"
msgstr "" msgstr "Version PC"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "On mobile" msgid "On mobile"
msgstr "" msgstr "Version mobile"
#: 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"
@@ -664,7 +668,7 @@ msgstr "Fichier OPML"
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "OPML file is required" msgid "OPML file is required"
msgstr "" msgstr "Vous devez fournir un fichier OPML"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Order" msgid "Order"
@@ -808,7 +812,7 @@ msgstr "Afficher les options de l'entrée (mobile)"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show external link icon" msgid "Show external link icon"
msgstr "" msgstr "Afficher l'icône du lien distant"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries" msgid "Show feeds and categories with no unread entries"
@@ -824,7 +828,15 @@ msgstr "Afficher les options du navigateur (ordinateur)"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr "Afficher l'icône Favori"
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr "Afficher le nombre d'entrées non lues dans la favicône de l'onglet"
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr "Afficher le nombre d'entrées non lues dans le titre de l'onglet"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr "Для браузера Chrome требуется расширение"
msgid "Browser extention" msgid "Browser extention"
msgstr "Расширение для браузера" msgstr "Расширение для браузера"
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr "Показать родное меню (ПК)"
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr ""
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr ""
msgid "Browser extention" msgid "Browser extention"
msgstr "Tarayıcı eklentisi" msgstr "Tarayıcı eklentisi"
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr "Orijinal tarayıcı menüsünü göster (masaüstü)"
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -140,6 +140,10 @@ msgstr "浏览器扩展"
msgid "Browser extention" msgid "Browser extention"
msgstr "浏览器扩展" msgstr "浏览器扩展"
#: src/components/settings/DisplaySettings.tsx
msgid "Browser tab"
msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -826,6 +830,14 @@ msgstr "显示原生菜单(桌面端)"
msgid "Show star icon" msgid "Show star icon"
msgstr "显示星标图标" msgstr "显示星标图标"
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx

View File

@@ -9,7 +9,7 @@ import { ActionButton } from "components/ActionButton"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { SiGithub, SiTwitter } from "react-icons/si" import { SiGithub, SiX } from "react-icons/si"
import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb" import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb"
import { PageTitle } from "./PageTitle" import { PageTitle } from "./PageTitle"
@@ -140,8 +140,8 @@ function Footer() {
<Anchor variant="text" href="https://github.com/Athou/commafeed/" target="_blank" rel="noreferrer"> <Anchor variant="text" href="https://github.com/Athou/commafeed/" target="_blank" rel="noreferrer">
<SiGithub /> <SiGithub />
</Anchor> </Anchor>
<Anchor variant="text" href="https://twitter.com/CommaFeed" target="_blank" rel="noreferrer"> <Anchor variant="text" href="https://x.com/CommaFeed" target="_blank" rel="noreferrer">
<SiTwitter /> <SiX />
</Anchor> </Anchor>
</Group> </Group>
<Box> <Box>

View File

@@ -0,0 +1,617 @@
:summaryTableId: commafeed-server_commafeed
[.configuration-legend]
icon:lock[title=Fixed at build time] Configuration property fixed at build time - All other configuration properties are overridable at runtime
[.configuration-reference.searchable, cols="80,.^10,.^10"]
|===
h|[.header-title]##Configuration property##
h|Type
h|Default
a| [[commafeed-server_commafeed-hide-from-web-crawlers]] [.property-path]##`commafeed.hide-from-web-crawlers`##
[.description]
--
Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HIDE_FROM_WEB_CRAWLERS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HIDE_FROM_WEB_CRAWLERS+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`true`
a| [[commafeed-server_commafeed-image-proxy-enabled]] [.property-path]##`commafeed.image-proxy-enabled`##
[.description]
--
If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser. This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_IMAGE_PROXY_ENABLED+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_IMAGE_PROXY_ENABLED+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`false`
a| [[commafeed-server_commafeed-password-recovery-enabled]] [.property-path]##`commafeed.password-recovery-enabled`##
[.description]
--
Enable password recovery via email. Quarkus mailer will need to be configured.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_PASSWORD_RECOVERY_ENABLED+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_PASSWORD_RECOVERY_ENABLED+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`false`
a| [[commafeed-server_commafeed-announcement]] [.property-path]##`commafeed.announcement`##
[.description]
--
Message displayed in a notification at the bottom of the page.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_ANNOUNCEMENT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_ANNOUNCEMENT+++`
endif::add-copy-button-to-env-var[]
--
|string
|
a| [[commafeed-server_commafeed-google-analytics-tracking-code]] [.property-path]##`commafeed.google-analytics-tracking-code`##
[.description]
--
Google Analytics tracking code.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_GOOGLE_ANALYTICS_TRACKING_CODE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_GOOGLE_ANALYTICS_TRACKING_CODE+++`
endif::add-copy-button-to-env-var[]
--
|string
|
a| [[commafeed-server_commafeed-google-auth-key]] [.property-path]##`commafeed.google-auth-key`##
[.description]
--
Google Auth key for fetching Youtube channel favicons.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_GOOGLE_AUTH_KEY+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_GOOGLE_AUTH_KEY+++`
endif::add-copy-button-to-env-var[]
--
|string
|
h|[[commafeed-server_section_commafeed-http-client]] [.section-name.section-level0]##HTTP client configuration##
h|Type
h|Default
a| [[commafeed-server_commafeed-http-client-user-agent]] [.property-path]##`commafeed.http-client.user-agent`##
[.description]
--
User-Agent string that will be used by the http client, leave empty for the default one.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_USER_AGENT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_USER_AGENT+++`
endif::add-copy-button-to-env-var[]
--
|string
|
a| [[commafeed-server_commafeed-http-client-connect-timeout]] [.property-path]##`commafeed.http-client.connect-timeout`##
[.description]
--
Time to wait for a connection to be established.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CONNECT_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CONNECT_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Time to wait for SSL handshake to complete.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_SSL_HANDSHAKE_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_SSL_HANDSHAKE_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Time to wait between two packets before timeout.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_SOCKET_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_SOCKET_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Time to wait for the full response to be received.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_RESPONSE_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_RESPONSE_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Time to live for a connection in the pool.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CONNECTION_TIME_TO_LIVE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CONNECTION_TIME_TO_LIVE+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Time between eviction runs for idle connections.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_IDLE_CONNECTIONS_EVICTION_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_IDLE_CONNECTIONS_EVICTION_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_MAX_RESPONSE_SIZE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_HTTP_CLIENT_MAX_RESPONSE_SIZE+++`
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-feed-refresh]] [.section-name.section-level0]##Feed refresh engine settings##
h|Type
h|Default
a| [[commafeed-server_commafeed-feed-refresh-interval]] [.property-path]##`commafeed.feed-refresh.interval`##
[.description]
--
Amount of time CommaFeed will wait before refreshing the same feed.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
If true, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since the last entry was published. The interval will be somewhere between the default refresh interval and 24h. See `FeedRefreshIntervalCalculator` for details.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`false`
a| [[commafeed-server_commafeed-feed-refresh-http-threads]] [.property-path]##`commafeed.feed-refresh.http-threads`##
[.description]
--
Amount of http threads used to fetch feeds.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_HTTP_THREADS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_HTTP_THREADS+++`
endif::add-copy-button-to-env-var[]
--
|@jakarta.validation.constraints.Min(1L) int
|`3`
a| [[commafeed-server_commafeed-feed-refresh-database-threads]] [.property-path]##`commafeed.feed-refresh.database-threads`##
[.description]
--
Amount of threads used to insert new entries in the database.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_DATABASE_THREADS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_DATABASE_THREADS+++`
endif::add-copy-button-to-env-var[]
--
|@jakarta.validation.constraints.Min(1L) int
|`1`
a| [[commafeed-server_commafeed-feed-refresh-user-inactivity-period]] [.property-path]##`commafeed.feed-refresh.user-inactivity-period`##
[.description]
--
Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_USER_INACTIVITY_PERIOD+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_USER_INACTIVITY_PERIOD+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_FILTERING_EXPRESSION_EVALUATION_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_FEED_REFRESH_FILTERING_EXPRESSION_EVALUATION_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`500MS`
h|[[commafeed-server_section_commafeed-database]] [.section-name.section-level0]##Database settings##
h|Type
h|Default
a| [[commafeed-server_commafeed-database-query-timeout]] [.property-path]##`commafeed.database.query-timeout`##
[.description]
--
Timeout applied to all database queries. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_QUERY_TIMEOUT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_QUERY_TIMEOUT+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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|Type
h|Default
a| [[commafeed-server_commafeed-database-cleanup-entries-max-age]] [.property-path]##`commafeed.database.cleanup.entries-max-age`##
[.description]
--
Maximum age of feed entries in the database. Older entries will be deleted. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_ENTRIES_MAX_AGE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_ENTRIES_MAX_AGE+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_STATUSES_MAX_AGE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_STATUSES_MAX_AGE+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
Maximum number of entries per feed to keep in the database. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_MAX_FEED_CAPACITY+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_MAX_FEED_CAPACITY+++`
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`##
[.description]
--
Limit the number of feeds a user can subscribe to. 0 to disable.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_MAX_FEEDS_PER_USER+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_MAX_FEEDS_PER_USER+++`
endif::add-copy-button-to-env-var[]
--
|int
|`0`
a| [[commafeed-server_commafeed-database-cleanup-batch-size]] [.property-path]##`commafeed.database.cleanup.batch-size`##
[.description]
--
Rows to delete per query while cleaning up old entries.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_BATCH_SIZE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_BATCH_SIZE+++`
endif::add-copy-button-to-env-var[]
--
|@jakarta.validation.constraints.Positive int
|`100`
h|[[commafeed-server_section_commafeed-users]] [.section-name.section-level0]##Users settings##
h|Type
h|Default
a| [[commafeed-server_commafeed-users-allow-registrations]] [.property-path]##`commafeed.users.allow-registrations`##
[.description]
--
Whether to let users create accounts for themselves.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_ALLOW_REGISTRATIONS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_USERS_ALLOW_REGISTRATIONS+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`false`
a| [[commafeed-server_commafeed-users-strict-password-policy]] [.property-path]##`commafeed.users.strict-password-policy`##
[.description]
--
Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_STRICT_PASSWORD_POLICY+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_USERS_STRICT_PASSWORD_POLICY+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`true`
a| [[commafeed-server_commafeed-users-create-demo-account]] [.property-path]##`commafeed.users.create-demo-account`##
[.description]
--
Whether to create a demo account the first time the app starts.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_CREATE_DEMO_ACCOUNT+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_USERS_CREATE_DEMO_ACCOUNT+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`false`
h|[[commafeed-server_section_commafeed-websocket]] [.section-name.section-level0]##Websocket settings##
h|Type
h|Default
a| [[commafeed-server_commafeed-websocket-enabled]] [.property-path]##`commafeed.websocket.enabled`##
[.description]
--
Enable websocket connection so the server can notify web clients that there are new entries for feeds.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_ENABLED+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_WEBSOCKET_ENABLED+++`
endif::add-copy-button-to-env-var[]
--
|boolean
|`true`
a| [[commafeed-server_commafeed-websocket-ping-interval]] [.property-path]##`commafeed.websocket.ping-interval`##
[.description]
--
Interval at which the client will send a ping message on the websocket to keep the connection alive.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_PING_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_WEBSOCKET_PING_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/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`##
[.description]
--
If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_TREE_RELOAD_INTERVAL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++COMMAFEED_WEBSOCKET_TREE_RELOAD_INTERVAL+++`
endif::add-copy-button-to-env-var[]
--
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|`30S`
|===
ifndef::no-duration-note[]
[NOTE]
[id=duration-note-anchor-commafeed-server_commafeed]
.About the Duration format
====
To write duration values, use the standard `java.time.Duration` format.
See the link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information.
You can also use a simplified format, starting with a number:
* If the value is only a number, it represents time in seconds.
* If the value is a number followed by `ms`, it represents time in milliseconds.
In other cases, the simplified format is translated to the `java.time.Duration` format for parsing:
* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`.
* If the value is a number followed by `d`, it is prefixed with `P`.
====
endif::no-duration-note[]
ifndef::no-memory-size-note[]
[NOTE]
[id=memory-size-note-anchor-commafeed-server_commafeed]
.About the MemorySize format
====
A size configuration option recognizes strings in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`.
If no suffix is given, assume bytes.
====
ifndef::no-memory-size-note[]
:!summaryTableId:

View File

@@ -6,15 +6,16 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>5.0.0</version> <version>5.1.0</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
<properties> <properties>
<quarkus.version>3.13.2</quarkus.version> <quarkus.version>3.14.1</quarkus.version>
<querydsl.version>6.6</querydsl.version> <querydsl.version>6.7</querydsl.version>
<rome.version>2.1.0</rome.version> <rome.version>2.1.0</rome.version>
<swagger.version>2.2.23</swagger.version>
<properties-plugin.version>1.2.1</properties-plugin.version> <properties-plugin.version>1.2.1</properties-plugin.version>
<build.database>h2</build.database> <build.database>h2</build.database>
@@ -43,7 +44,7 @@
<plugins> <plugins>
<plugin> <plugin>
<artifactId>maven-help-plugin</artifactId> <artifactId>maven-help-plugin</artifactId>
<version>3.4.1</version> <version>3.5.0</version>
<executions> <executions>
<execution> <execution>
<phase>initialize</phase> <phase>initialize</phase>
@@ -78,6 +79,21 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-config-doc-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<id>default-generate-asciidoc</id>
<phase>process-test-resources</phase>
<goals>
<goal>generate-asciidoc</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId> <artifactId>maven-assembly-plugin</artifactId>
@@ -101,7 +117,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>3.3.1</version> <version>3.5.0</version>
<configuration> <configuration>
<systemPropertyVariables> <systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
@@ -111,7 +127,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId> <artifactId>maven-failsafe-plugin</artifactId>
<version>3.3.1</version> <version>3.5.0</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>
@@ -150,7 +166,7 @@
<plugin> <plugin>
<groupId>io.swagger.core.v3</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-maven-plugin-jakarta</artifactId> <artifactId>swagger-maven-plugin-jakarta</artifactId>
<version>2.2.22</version> <version>${swagger.version}</version>
<?m2e ignore?> <?m2e ignore?>
<configuration> <configuration>
<outputPath>${project.build.directory}/classes/META-INF/resources</outputPath> <outputPath>${project.build.directory}/classes/META-INF/resources</outputPath>
@@ -174,7 +190,14 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId> <artifactId>maven-checkstyle-plugin</artifactId>
<version>3.4.0</version> <version>3.5.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.18.1</version>
</dependency>
</dependencies>
<executions> <executions>
<execution> <execution>
<id>validate</id> <id>validate</id>
@@ -228,7 +251,7 @@
<dependency> <dependency>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<version>5.0.0</version> <version>5.1.0</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -296,17 +319,22 @@
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId> <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</dependency>
<dependency> <dependency>
<groupId>io.dropwizard.metrics</groupId> <groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId> <artifactId>metrics-json</artifactId>
<version>4.2.26</version> <version>4.2.27</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.swagger.core.v3</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId> <artifactId>swagger-annotations</artifactId>
<version>2.2.22</version> <version>${swagger.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -322,6 +350,10 @@
<version>${querydsl.version}</version> <version>${querydsl.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId> <artifactId>commons-collections4</artifactId>
@@ -346,7 +378,7 @@
<dependency> <dependency>
<groupId>org.passay</groupId> <groupId>org.passay</groupId>
<artifactId>passay</artifactId> <artifactId>passay</artifactId>
<version>1.6.4</version> <version>1.6.5</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -413,12 +445,6 @@
<version>8.3.6</version> <version>8.3.6</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-youtube</artifactId>
<version>v3-rev20240514-2.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId> <artifactId>quarkus-junit5</artifactId>

View File

@@ -1,5 +1,4 @@
FROM ibm-semeru-runtimes:open-21.0.4_7-jre FROM ibm-semeru-runtimes:open-21.0.4_7-jre
EXPOSE 8082 EXPOSE 8082
RUN mkdir -p /commafeed/data RUN mkdir -p /commafeed/data

View File

@@ -6,4 +6,5 @@ VOLUME /commafeed/data
COPY commafeed-server/target/commafeed-*-runner /commafeed/application COPY commafeed-server/target/commafeed-*-runner /commafeed/application
WORKDIR /commafeed WORKDIR /commafeed
CMD ["./application"] CMD ["./application"]

View File

@@ -4,7 +4,7 @@ Official docker images for https://github.com/Athou/commafeed/
## Quickstart ## Quickstart
Start CommaFeed with an embedded database. Then login as `admin/admin` on http://localhost:8082/ Start CommaFeed with a H2 embedded database. Then login as `admin/admin` on http://localhost:8082/
### docker ### docker
@@ -29,8 +29,8 @@ services:
## Advanced ## Advanced
While using the embedded database is perfectly fine for small instances, you may want to have more control over the While using the H2 embedded database is perfectly fine for small instances, you may want to have more control over the
database. Here's an example that uses postgresql (note the different docker tag): database. Here's an example that uses PostgreSQL (note the image tag change from `latest-h2` to `latest-postgresql`):
``` ```
services: services:
@@ -59,28 +59,34 @@ services:
- /path/to/commafeed/db:/var/lib/postgresql/data - /path/to/commafeed/db:/var/lib/postgresql/data
``` ```
CommaFeed also supports:
- MySQL:
`QUARKUS_DATASOURCE_JDBC_URL=jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
- MariaDB:
`QUARKUS_DATASOURCE_JDBC_URL=jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
## Configuration ## Configuration
All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/doc/commafeed.adoc) are
are optional and have sensible default values. optional and have sensible default values.
Settings are overrideable with environment variables. For instance, `config.feedRefresh().intervalEmpirical()` can be Settings are overrideable with environment variables. For instance, `commafeed.feed-refresh.interval-empirical` can be
set set with the `COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL` variable.
with the `COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL=true` variable.
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup, When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
`QUARKUS_HTTP_AUTH_SESSION_ENCRYPTION_KEY` property to a fixed value (min. 16 characters). `QUARKUS_HTTP_AUTH_SESSION_ENCRYPTION_KEY` variable to a fixed value (min. 16 characters).
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
## Docker tags ## Docker tags
Tags are of the form `<version>-<database>[-jvm]` where: Tags are of the form `<version>-<database>[-jvm]` where:
- `<version>` is either: - `<version>` is either:
- a specific CommaFeed version (e.g. `4.6.0`) - a specific CommaFeed version (e.g. `5.0.0`)
- `latest` (always points to the latest version) - `latest` (always points to the latest version)
- `master` (always points to the latest git commit) - `master` (always points to the latest git commit)
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`) - `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. This image supports - `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. This image supports
the the arm64 platform which is not yet supported by the native image.
arm64 platform which is not yet supported by the native image.

View File

@@ -6,6 +6,9 @@ import java.util.Optional;
import com.commafeed.backend.feed.FeedRefreshIntervalCalculator; import com.commafeed.backend.feed.FeedRefreshIntervalCalculator;
import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.quarkus.runtime.configuration.MemorySize; import io.quarkus.runtime.configuration.MemorySize;
import io.smallrye.config.ConfigMapping; import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault; import io.smallrye.config.WithDefault;
@@ -18,6 +21,7 @@ import jakarta.validation.constraints.Positive;
* Default values are for production, they can be overridden in application.properties for other profiles * Default values are for production, they can be overridden in application.properties for other profiles
*/ */
@ConfigMapping(prefix = "commafeed") @ConfigMapping(prefix = "commafeed")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public interface CommaFeedConfiguration { public interface CommaFeedConfiguration {
/** /**
* Whether to expose a robots.txt file that disallows web crawlers and search engine indexers. * Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
@@ -59,26 +63,31 @@ public interface CommaFeedConfiguration {
/** /**
* HTTP client configuration * HTTP client configuration
*/ */
@ConfigDocSection
HttpClient httpClient(); HttpClient httpClient();
/** /**
* Feed refresh engine settings. * Feed refresh engine settings.
*/ */
@ConfigDocSection
FeedRefresh feedRefresh(); FeedRefresh feedRefresh();
/** /**
* Database settings. * Database settings.
*/ */
@ConfigDocSection
Database database(); Database database();
/** /**
* Users settings. * Users settings.
*/ */
@ConfigDocSection
Users users(); Users users();
/** /**
* Websocket settings. * Websocket settings.
*/ */
@ConfigDocSection
Websocket websocket(); Websocket websocket();
interface HttpClient { interface HttpClient {
@@ -177,13 +186,17 @@ public interface CommaFeedConfiguration {
interface Database { interface Database {
/** /**
* Database query timeout. * Timeout applied to all database queries.
* *
* 0 to disable. * 0 to disable.
*/ */
@WithDefault("0") @WithDefault("0")
Duration queryTimeout(); Duration queryTimeout();
/**
* Database cleanup settings.
*/
@ConfigDocSection
Cleanup cleanup(); Cleanup cleanup();
interface Cleanup { interface Cleanup {

View File

@@ -1,28 +1,25 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.net.URIBuilder; import org.apache.hc.core5.net.URIBuilder;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.google.api.client.http.javanet.NetHttpTransport; import com.fasterxml.jackson.core.JsonPointer;
import com.google.api.client.json.gson.GsonFactory; import com.fasterxml.jackson.databind.JsonNode;
import com.google.api.services.youtube.YouTube; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.services.youtube.YouTube.Channels;
import com.google.api.services.youtube.YouTube.Playlists;
import com.google.api.services.youtube.model.Channel;
import com.google.api.services.youtube.model.ChannelListResponse;
import com.google.api.services.youtube.model.PlaylistListResponse;
import com.google.api.services.youtube.model.Thumbnail;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import jakarta.ws.rs.core.UriBuilder;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -31,13 +28,16 @@ import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
private static final JsonPointer CHANNEL_THUMBNAIL_URL = JsonPointer.compile("/items/0/snippet/thumbnails/default/url");
private static final JsonPointer PLAYLIST_CHANNEL_ID = JsonPointer.compile("/items/0/snippet/channelId");
private final HttpGetter getter; private final HttpGetter getter;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private final ObjectMapper objectMapper;
@Override @Override
public Favicon fetch(Feed feed) { public Favicon fetch(Feed feed) {
String url = feed.getUrl(); String url = feed.getUrl();
if (!url.toLowerCase().contains("youtube.com/feeds/videos.xml")) { if (!url.toLowerCase().contains("youtube.com/feeds/videos.xml")) {
return null; return null;
} }
@@ -56,35 +56,33 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
Optional<NameValuePair> channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst(); Optional<NameValuePair> channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst();
Optional<NameValuePair> playlistId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id")).findFirst(); Optional<NameValuePair> playlistId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id")).findFirst();
YouTube youtube = new YouTube.Builder(new NetHttpTransport(), GsonFactory.getDefaultInstance(), request -> { byte[] response = null;
}).setApplicationName("CommaFeed").build();
ChannelListResponse response = null;
if (userId.isPresent()) { if (userId.isPresent()) {
log.debug("contacting youtube api for user {}", userId.get().getValue()); log.debug("contacting youtube api for user {}", userId.get().getValue());
response = fetchForUser(youtube, googleAuthKey.get(), userId.get().getValue()); response = fetchForUser(googleAuthKey.get(), userId.get().getValue());
} else if (channelId.isPresent()) { } else if (channelId.isPresent()) {
log.debug("contacting youtube api for channel {}", channelId.get().getValue()); log.debug("contacting youtube api for channel {}", channelId.get().getValue());
response = fetchForChannel(youtube, googleAuthKey.get(), channelId.get().getValue()); response = fetchForChannel(googleAuthKey.get(), channelId.get().getValue());
} else if (playlistId.isPresent()) { } else if (playlistId.isPresent()) {
log.debug("contacting youtube api for playlist {}", playlistId.get().getValue()); log.debug("contacting youtube api for playlist {}", playlistId.get().getValue());
response = fetchForPlaylist(youtube, googleAuthKey.get(), playlistId.get().getValue()); response = fetchForPlaylist(googleAuthKey.get(), playlistId.get().getValue());
} }
if (ArrayUtils.isEmpty(response)) {
if (response == null || response.isEmpty() || CollectionUtils.isEmpty(response.getItems())) { log.debug("youtube api returned empty response");
log.debug("youtube api returned no items");
return null; return null;
} }
Channel channel = response.getItems().get(0); JsonNode thumbnailUrl = objectMapper.readTree(response).at(CHANNEL_THUMBNAIL_URL);
Thumbnail thumbnail = channel.getSnippet().getThumbnails().getDefault(); if (thumbnailUrl.isMissingNode()) {
log.debug("youtube api returned invalid response");
return null;
}
log.debug("fetching favicon"); HttpResult iconResult = getter.getBinary(thumbnailUrl.asText());
HttpResult iconResult = getter.getBinary(thumbnail.getUrl());
bytes = iconResult.getContent(); bytes = iconResult.getContent();
contentType = iconResult.getContentType(); contentType = iconResult.getContentType();
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to retrieve YouTube icon", e); log.error("Failed to retrieve YouTube icon", e);
} }
if (!isValidIconResponse(bytes, contentType)) { if (!isValidIconResponse(bytes, contentType)) {
@@ -93,32 +91,38 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
return new Favicon(bytes, contentType); return new Favicon(bytes, contentType);
} }
private ChannelListResponse fetchForUser(YouTube youtube, String googleAuthKey, String userId) throws IOException { private byte[] fetchForUser(String googleAuthKey, String userId) throws IOException, NotModifiedException {
Channels.List list = youtube.channels().list(List.of("snippet")); URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
list.setKey(googleAuthKey); .queryParam("part", "snippet")
list.setForUsername(userId); .queryParam("key", googleAuthKey)
return list.execute(); .queryParam("forUsername", userId)
.build();
return getter.getBinary(uri.toString()).getContent();
} }
private ChannelListResponse fetchForChannel(YouTube youtube, String googleAuthKey, String channelId) throws IOException { private byte[] fetchForChannel(String googleAuthKey, String channelId) throws IOException, NotModifiedException {
Channels.List list = youtube.channels().list(List.of("snippet")); URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
list.setKey(googleAuthKey); .queryParam("part", "snippet")
list.setId(List.of(channelId)); .queryParam("key", googleAuthKey)
return list.execute(); .queryParam("id", channelId)
.build();
return getter.getBinary(uri.toString()).getContent();
} }
private ChannelListResponse fetchForPlaylist(YouTube youtube, String googleAuthKey, String playlistId) throws IOException { private byte[] fetchForPlaylist(String googleAuthKey, String playlistId) throws IOException, NotModifiedException {
Playlists.List list = youtube.playlists().list(List.of("snippet")); URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
list.setKey(googleAuthKey); .queryParam("part", "snippet")
list.setId(List.of(playlistId)); .queryParam("key", googleAuthKey)
.queryParam("id", playlistId)
.build();
byte[] playlistBytes = getter.getBinary(uri.toString()).getContent();
PlaylistListResponse response = list.execute(); JsonNode channelId = objectMapper.readTree(playlistBytes).at(PLAYLIST_CHANNEL_ID);
if (response.getItems().isEmpty()) { if (channelId.isMissingNode()) {
return null; return null;
} }
String channelId = response.getItems().get(0).getSnippet().getChannelId(); return fetchForChannel(googleAuthKey, channelId.asText());
return fetchForChannel(youtube, googleAuthKey, channelId);
} }
} }

View File

@@ -1,10 +1,11 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
@@ -48,7 +49,7 @@ public class FeedFetcher {
parserResult = parser.parse(result.getUrlAfterRedirect(), content); parserResult = parser.parse(result.getUrlAfterRedirect(), content);
} catch (FeedException e) { } catch (FeedException e) {
if (extractFeedUrlFromHtml) { if (extractFeedUrlFromHtml) {
String extractedUrl = extractFeedUrl(urlProviders, feedUrl, StringUtils.newStringUtf8(result.getContent())); String extractedUrl = extractFeedUrl(urlProviders, feedUrl, new String(result.getContent(), StandardCharsets.UTF_8));
if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) { if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) {
feedUrl = extractedUrl; feedUrl = extractedUrl;

View File

@@ -6,8 +6,8 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.utils.Base64;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;

View File

@@ -1,9 +1,13 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.sql.Types;
import java.time.Instant; import java.time.Instant;
import org.hibernate.annotations.JdbcTypeCode;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Lob;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -18,7 +22,9 @@ public class Feed extends AbstractModel {
/** /**
* The url of the feed * The url of the feed
*/ */
@Column(length = 2048, nullable = false) @Lob
@Column(length = Integer.MAX_VALUE, nullable = false)
@JdbcTypeCode(Types.LONGVARCHAR)
private String url; private String url;
/** /**
@@ -36,7 +42,9 @@ public class Feed extends AbstractModel {
/** /**
* The url of the website, extracted from the feed * The url of the website, extracted from the feed
*/ */
@Column(length = 2048) @Lob
@Column(length = Integer.MAX_VALUE)
@JdbcTypeCode(Types.LONGVARCHAR)
private String link; private String link;
/** /**
@@ -60,7 +68,9 @@ public class Feed extends AbstractModel {
/** /**
* error message while retrieving the feed * error message while retrieving the feed
*/ */
@Column(length = 1024) @Lob
@Column(length = Integer.MAX_VALUE)
@JdbcTypeCode(Types.LONGVARCHAR)
private String message; private String message;
/** /**

View File

@@ -1,9 +1,13 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.sql.Types;
import java.time.Instant; import java.time.Instant;
import org.hibernate.annotations.JdbcTypeCode;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Lob;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -21,13 +25,17 @@ public class User extends AbstractModel {
@Column(length = 255, unique = true) @Column(length = 255, unique = true)
private String email; private String email;
@Column(length = 256, nullable = false) @Lob
@Column(length = Integer.MAX_VALUE, nullable = false)
@JdbcTypeCode(Types.LONGVARBINARY)
private byte[] password; private byte[] password;
@Column(length = 40, unique = true) @Column(length = 40, unique = true)
private String apiKey; private String apiKey;
@Column(length = 8, nullable = false) @Lob
@Column(length = Integer.MAX_VALUE, nullable = false)
@JdbcTypeCode(Types.LONGVARBINARY)
private byte[] salt; private byte[] salt;
@Column(nullable = false) @Column(nullable = false)

View File

@@ -89,6 +89,8 @@ public class UserSettings extends AbstractModel {
private boolean markAllAsReadConfirmation; private boolean markAllAsReadConfirmation;
private boolean customContextMenu; private boolean customContextMenu;
private boolean mobileFooter; private boolean mobileFooter;
private boolean unreadCountTitle;
private boolean unreadCountFavicon;
private boolean email; private boolean email;
private boolean gmail; private boolean gmail;

View File

@@ -59,12 +59,12 @@ public class DatabaseCleaningService {
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize)); entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
entriesDeletedMeter.mark(entriesDeleted); entriesDeletedMeter.mark(entriesDeleted);
entriesTotal += entriesDeleted; entriesTotal += entriesDeleted;
log.info("removed {} entries for feeds without subscriptions", entriesTotal); log.debug("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0); } while (entriesDeleted > 0);
} }
deleted = unitOfWork.call(() -> feedDAO.delete(feedDAO.findByIds(feeds.stream().map(AbstractModel::getId).toList()))); deleted = unitOfWork.call(() -> feedDAO.delete(feedDAO.findByIds(feeds.stream().map(AbstractModel::getId).toList())));
total += deleted; total += deleted;
log.info("removed {} feeds without subscriptions", total); log.debug("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} feeds without subscriptions deleted", total); log.info("cleanup done: {} feeds without subscriptions deleted", total);
} }
@@ -76,7 +76,7 @@ public class DatabaseCleaningService {
do { do {
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize)); deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize));
total += deleted; total += deleted;
log.info("removed {} contents without entries", total); log.debug("removed {} contents without entries", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} contents without entries deleted", total); log.info("cleanup done: {} contents without entries deleted", total);
} }
@@ -98,7 +98,7 @@ public class DatabaseCleaningService {
entriesDeletedMeter.mark(deleted); entriesDeletedMeter.mark(deleted);
total += deleted; total += deleted;
remaining -= deleted; remaining -= deleted;
log.info("removed {} entries for feeds exceeding capacity", total); log.debug("removed {} entries for feeds exceeding capacity", total);
} while (remaining > 0); } while (remaining > 0);
} }
} }
@@ -113,7 +113,7 @@ public class DatabaseCleaningService {
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize)); deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize));
entriesDeletedMeter.mark(deleted); entriesDeletedMeter.mark(deleted);
total += deleted; total += deleted;
log.info("removed {} old entries", total); log.debug("removed {} old entries", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} old entries deleted", total); log.info("cleanup done: {} old entries deleted", total);
} }
@@ -125,7 +125,7 @@ public class DatabaseCleaningService {
do { do {
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize)); deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
total += deleted; total += deleted;
log.info("removed {} old read statuses", total); log.debug("removed {} old read statuses", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} old read statuses deleted", total); log.info("cleanup done: {} old read statuses deleted", total);
} }

View File

@@ -70,6 +70,12 @@ public class Settings implements Serializable {
@Schema(description = "on mobile, show action buttons at the bottom of the screen", requiredMode = RequiredMode.REQUIRED) @Schema(description = "on mobile, show action buttons at the bottom of the screen", requiredMode = RequiredMode.REQUIRED)
private boolean mobileFooter; private boolean mobileFooter;
@Schema(description = "show unread count in the title", requiredMode = RequiredMode.REQUIRED)
private boolean unreadCountTitle;
@Schema(description = "show unread count in the favicon", requiredMode = RequiredMode.REQUIRED)
private boolean unreadCountFavicon;
@Schema(description = "sharing settings", requiredMode = RequiredMode.REQUIRED) @Schema(description = "sharing settings", requiredMode = RequiredMode.REQUIRED)
private SharingSettings sharingSettings = new SharingSettings(); private SharingSettings sharingSettings = new SharingSettings();

View File

@@ -241,7 +241,7 @@ public class FeedREST {
} catch (Exception e) { } catch (Exception e) {
log.debug(e.getMessage(), e); log.debug(e.getMessage(), e);
throw new WebApplicationException(e, Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build()); throw new WebApplicationException(e.getMessage(), Status.INTERNAL_SERVER_ERROR);
} }
return info; return info;
} }

View File

@@ -9,7 +9,7 @@ import io.swagger.v3.oas.annotations.servers.Server;
@OpenAPIDefinition( @OpenAPIDefinition(
info = @Info(title = "CommaFeed API"), info = @Info(title = "CommaFeed API"),
servers = { @Server(description = "CommaFeed API", url = "rest") }, servers = { @Server(description = "CommaFeed API", url = "/") },
security = { @SecurityRequirement(name = "basicAuth") }) security = { @SecurityRequirement(name = "basicAuth") })
@SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic") @SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic")
public class OpenAPI { public class OpenAPI {

View File

@@ -119,6 +119,8 @@ public class UserREST {
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation()); s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
s.setCustomContextMenu(settings.isCustomContextMenu()); s.setCustomContextMenu(settings.isCustomContextMenu());
s.setMobileFooter(settings.isMobileFooter()); s.setMobileFooter(settings.isMobileFooter());
s.setUnreadCountTitle(settings.isUnreadCountTitle());
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
} else { } else {
s.setReadingMode(ReadingMode.unread.name()); s.setReadingMode(ReadingMode.unread.name());
s.setReadingOrder(ReadingOrder.desc.name()); s.setReadingOrder(ReadingOrder.desc.name());
@@ -142,6 +144,8 @@ public class UserREST {
s.setMarkAllAsReadConfirmation(true); s.setMarkAllAsReadConfirmation(true);
s.setCustomContextMenu(true); s.setCustomContextMenu(true);
s.setMobileFooter(false); s.setMobileFooter(false);
s.setUnreadCountTitle(false);
s.setUnreadCountFavicon(true);
} }
return Response.ok(s).build(); return Response.ok(s).build();
} }
@@ -173,6 +177,8 @@ public class UserREST {
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation()); s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
s.setCustomContextMenu(settings.isCustomContextMenu()); s.setCustomContextMenu(settings.isCustomContextMenu());
s.setMobileFooter(settings.isMobileFooter()); s.setMobileFooter(settings.isMobileFooter());
s.setUnreadCountTitle(settings.isUnreadCountTitle());
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
s.setEmail(settings.getSharingSettings().isEmail()); s.setEmail(settings.getSharingSettings().isEmail());
s.setGmail(settings.getSharingSettings().isGmail()); s.setGmail(settings.getSharingSettings().isGmail());

View File

@@ -1,12 +1,12 @@
package com.commafeed.frontend.servlet; package com.commafeed.frontend.servlet;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import com.commafeed.security.AuthenticationContext; import com.commafeed.security.AuthenticationContext;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
@@ -20,16 +20,16 @@ public class CustomCssServlet {
private final AuthenticationContext authenticationContext; private final AuthenticationContext authenticationContext;
private final UserSettingsDAO userSettingsDAO; private final UserSettingsDAO userSettingsDAO;
private final UnitOfWork unitOfWork;
@GET @GET
@Transactional
public String get() { public String get() {
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
if (user == null) { if (user == null) {
return ""; return "";
} }
UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user)); UserSettings settings = userSettingsDAO.findByUser(user);
if (settings == null) { if (settings == null) {
return ""; return "";
} }

View File

@@ -1,12 +1,12 @@
package com.commafeed.frontend.servlet; package com.commafeed.frontend.servlet;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import com.commafeed.security.AuthenticationContext; import com.commafeed.security.AuthenticationContext;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
@@ -20,16 +20,16 @@ public class CustomJsServlet {
private final AuthenticationContext authenticationContext; private final AuthenticationContext authenticationContext;
private final UserSettingsDAO userSettingsDAO; private final UserSettingsDAO userSettingsDAO;
private final UnitOfWork unitOfWork;
@GET @GET
@Transactional
public String get() { public String get() {
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
if (user == null) { if (user == null) {
return ""; return "";
} }
UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user)); UserSettings settings = userSettingsDAO.findByUser(user);
if (settings == null) { if (settings == null) {
return ""; return "";
} }

View File

@@ -8,7 +8,6 @@ import org.apache.commons.lang3.StringUtils;
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.UnitOfWork;
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;
@@ -20,6 +19,7 @@ import com.commafeed.security.AuthenticationContext;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
@@ -33,7 +33,6 @@ import lombok.RequiredArgsConstructor;
@Singleton @Singleton
public class NextUnreadServlet { public class NextUnreadServlet {
private final UnitOfWork unitOfWork;
private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedCategoryDAO feedCategoryDAO; private final FeedCategoryDAO feedCategoryDAO;
@@ -42,36 +41,34 @@ public class NextUnreadServlet {
private final UriInfo uri; private final UriInfo uri;
@GET @GET
@Transactional
public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
if (user == null) { if (user == null) {
return Response.temporaryRedirect(uri.getBaseUri()).build(); return Response.temporaryRedirect(uri.getBaseUri()).build();
} }
FeedEntryStatus status = unitOfWork.call(() -> { FeedEntryStatus s = null;
FeedEntryStatus s = null; if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user); List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, null,
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, null, null);
null, null, null); s = Iterables.getFirst(statuses, null);
} else {
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
if (category != null) {
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, order,
true, null, null, null);
s = Iterables.getFirst(statuses, null); s = Iterables.getFirst(statuses, null);
} else {
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
if (category != null) {
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1,
order, true, null, null, null);
s = Iterables.getFirst(statuses, null);
}
} }
if (s != null) { }
feedEntryService.markEntry(user, s.getEntry().getId(), true); if (s != null) {
} feedEntryService.markEntry(user, s.getEntry().getId(), true);
return s; }
});
String url = status == null ? uri.getBaseUri().toString() : status.getEntry().getUrl(); String url = s == null ? uri.getBaseUri().toString() : s.getEntry().getUrl();
return Response.temporaryRedirect(URI.create(url)).build(); return Response.temporaryRedirect(URI.create(url)).build();
} }
} }

View File

@@ -1,20 +1,17 @@
package com.commafeed.security.mechanism; package com.commafeed.security.mechanism;
import java.security.SecureRandom; import io.quarkus.security.identity.IdentityProviderManager;
import java.util.Base64; import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.FormAuthConfig;
import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.PersistentLoginManager; import io.smallrye.mutiny.Uni;
import io.vertx.core.http.Cookie; import io.vertx.core.http.Cookie;
import io.vertx.core.http.impl.ServerCookie; import io.vertx.core.http.impl.ServerCookie;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
import jakarta.annotation.Priority; import jakarta.annotation.Priority;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Delegate; import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -24,67 +21,24 @@ import lombok.extern.slf4j.Slf4j;
* This is a workaround for https://github.com/quarkusio/quarkus/issues/42463 * This is a workaround for https://github.com/quarkusio/quarkus/issues/42463
*/ */
@Priority(1) @Priority(1)
@RequiredArgsConstructor
@Singleton @Singleton
@Slf4j @Slf4j
public class CookieMaxAgeFormAuthenticationMechanism implements HttpAuthenticationMechanism { public class CookieMaxAgeFormAuthenticationMechanism implements HttpAuthenticationMechanism {
// the temp encryption key, persistent across dev mode restarts
static volatile String encryptionKey;
@Delegate @Delegate
private final FormAuthenticationMechanism delegate; private final FormAuthenticationMechanism delegate;
private final HttpConfiguration config;
public CookieMaxAgeFormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { @Override
String key; public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
if (httpConfiguration.encryptionKey.isEmpty()) { context.addHeadersEndHandler(v -> {
if (encryptionKey != null) { Cookie cookie = context.request().getCookie(config.auth.form.cookieName);
// persist across dev mode restarts if (cookie instanceof ServerCookie sc && sc.isChanged()) {
key = encryptionKey; cookie.setMaxAge(config.auth.form.timeout.toSeconds());
} else {
byte[] data = new byte[32];
new SecureRandom().nextBytes(data);
key = encryptionKey = Base64.getEncoder().encodeToString(data);
log.warn("Encryption key was not specified for persistent FORM auth, using temporary key {}", key);
} }
} else { });
key = httpConfiguration.encryptionKey.get();
}
FormAuthConfig form = buildTimeConfig.auth.form; return delegate.authenticate(context, identityProviderManager);
FormAuthRuntimeConfig runtimeForm = httpConfiguration.auth.form;
String loginPage = startWithSlash(runtimeForm.loginPage.orElse(null));
String errorPage = startWithSlash(runtimeForm.errorPage.orElse(null));
String landingPage = startWithSlash(runtimeForm.landingPage.orElse(null));
String postLocation = startWithSlash(form.postLocation);
String usernameParameter = runtimeForm.usernameParameter;
String passwordParameter = runtimeForm.passwordParameter;
String locationCookie = runtimeForm.locationCookie;
String cookiePath = runtimeForm.cookiePath.orElse(null);
boolean redirectAfterLogin = landingPage != null;
String cookieSameSite = runtimeForm.cookieSameSite.name();
PersistentLoginManager loginManager = new PersistentLoginManager(key, runtimeForm.cookieName, runtimeForm.timeout.toMillis(),
runtimeForm.newCookieInterval.toMillis(), runtimeForm.httpOnlyCookie, cookieSameSite, cookiePath) {
@Override
public void save(String value, RoutingContext context, String cookieName, RestoreResult restoreResult, boolean secureCookie) {
super.save(value, context, cookieName, restoreResult, secureCookie);
// add max age to the cookie
Cookie cookie = context.request().getCookie(cookieName);
if (cookie instanceof ServerCookie sc && sc.isChanged()) {
cookie.setMaxAge(runtimeForm.timeout.toSeconds());
}
}
};
this.delegate = new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, errorPage,
landingPage, redirectAfterLogin, locationCookie, cookieSameSite, cookiePath, loginManager);
}
private static String startWithSlash(String page) {
if (page == null) {
return null;
}
return page.startsWith("/") ? page : "/" + page;
} }
} }

View File

@@ -2,6 +2,14 @@
quarkus.http.port=8082 quarkus.http.port=8082
quarkus.http.test-port=8085 quarkus.http.test-port=8085
# static files
## make sure the webapp is always up to date
quarkus.http.filter.index-html.header."Cache-Control"=no-cache
quarkus.http.filter.index-html.matches=/
## make sure the openapi documentation is always up to date
quarkus.http.filter.openapi.header."Cache-Control"=no-cache
quarkus.http.filter.openapi.matches=/openapi[.](json|yaml)
# security # security
quarkus.http.auth.basic=true quarkus.http.auth.basic=true
quarkus.http.auth.form.enabled=true quarkus.http.auth.form.enabled=true
@@ -22,6 +30,9 @@ quarkus.liquibase.migrate-at-start=true
# shutdown # shutdown
quarkus.shutdown.timeout=5s quarkus.shutdown.timeout=5s
# native
quarkus.native.march=compatibility
# dev profile overrides # dev profile overrides
%dev.quarkus.http.port=8083 %dev.quarkus.http.port=8083

View File

@@ -0,0 +1,28 @@
<?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-unread-count-settings" author="athou">
<addColumn tableName="USERSETTINGS">
<column name="unreadCountTitle" type="BOOLEAN" value="false">
<constraints nullable="false" />
</column>
<column name="unreadCountFavicon" type="BOOLEAN" value="true">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>
<changeSet id="add-missing-fk-on-statuses-user" author="athou">
<delete tableName="FEEDENTRYSTATUSES">
<where>user_id not in (select id from USERS)</where>
</delete>
<addForeignKeyConstraint baseTableName="FEEDENTRYSTATUSES"
baseColumnNames="user_id"
constraintName="fk_feedentrystatuses_user"
referencedTableName="USERS"
referencedColumnNames="id" />
</changeSet>
</databaseChangeLog>

View File

@@ -31,5 +31,6 @@
<include file="changelogs/db.changelog-4.2.xml" /> <include file="changelogs/db.changelog-4.2.xml" />
<include file="changelogs/db.changelog-4.3.xml" /> <include file="changelogs/db.changelog-4.3.xml" />
<include file="changelogs/db.changelog-4.4.xml" /> <include file="changelogs/db.changelog-4.4.xml" />
<include file="changelogs/db.changelog-5.1.xml" />
</databaseChangeLog> </databaseChangeLog>

View File

@@ -0,0 +1,22 @@
package com.commafeed;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class CommaFeedConfigurationTest {
@Test
void verifyAsciiDocIsUpToDate() throws IOException {
String versionedDocumentationFile = FileUtils.readFileToString(new File("doc/commafeed.adoc"), StandardCharsets.UTF_8);
String generatedDocumentationFile = FileUtils
.readFileToString(new File("target/quarkus-generated-doc/config/commafeed-server.adoc"), StandardCharsets.UTF_8);
Assertions.assertLinesMatch(versionedDocumentationFile.lines(), generatedDocumentationFile.lines());
}
}

View File

@@ -4,12 +4,10 @@ import org.kohsuke.MetaInfServices;
import com.commafeed.backend.service.db.DatabaseStartupService; import com.commafeed.backend.service.db.DatabaseStartupService;
import io.quarkus.liquibase.LiquibaseFactory; import io.quarkus.liquibase.runtime.LiquibaseSchemaProvider;
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestMethodContext; import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
import jakarta.enterprise.inject.spi.CDI; import jakarta.enterprise.inject.spi.CDI;
import liquibase.Liquibase;
import liquibase.exception.LiquibaseException;
/** /**
* Resets database between tests * Resets database between tests
@@ -17,18 +15,9 @@ import liquibase.exception.LiquibaseException;
@MetaInfServices @MetaInfServices
public class DatabaseReset implements QuarkusTestBeforeEachCallback { public class DatabaseReset implements QuarkusTestBeforeEachCallback {
@SuppressWarnings("deprecation")
@Override @Override
public void beforeEach(QuarkusTestMethodContext context) { public void beforeEach(QuarkusTestMethodContext context) {
LiquibaseFactory liquibaseFactory = CDI.current().select(LiquibaseFactory.class).get(); new LiquibaseSchemaProvider().resetAllDatabases();
try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { CDI.current().select(DatabaseStartupService.class).get().populateInitialData();
liquibase.dropAll();
liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels());
} catch (LiquibaseException e) {
throw new RuntimeException(e);
}
DatabaseStartupService databaseStartupService = CDI.current().select(DatabaseStartupService.class).get();
databaseStartupService.populateInitialData();
} }
} }

View File

@@ -1,19 +1,25 @@
package com.commafeed.backend; package com.commafeed.backend;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.hc.client5.http.ConnectTimeoutException; import org.apache.hc.client5.http.ConnectTimeoutException;
import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@@ -56,10 +62,10 @@ class HttpGetterTest {
this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.httpClient().userAgent()).thenReturn(Optional.of("http-getter-test")); Mockito.when(config.httpClient().userAgent()).thenReturn(Optional.of("http-getter-test"));
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500)); Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(5)); Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofMillis(500)); Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(300)); Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30)); Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000"))); Mockito.when(config.httpClient().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000")));
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3); Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
@@ -121,6 +127,9 @@ class HttpGetterTest {
@Test @Test
void dataTimeout() { void dataTimeout() {
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500));
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
this.mockServerClient.when(HttpRequest.request().withMethod("GET")) this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000))); .respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
@@ -129,6 +138,8 @@ class HttpGetterTest {
@Test @Test
void connectTimeout() { void connectTimeout() {
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500));
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
// try to connect to a non-routable address // try to connect to a non-routable address
// https://stackoverflow.com/a/904609 // https://stackoverflow.com/a/904609
Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1")); Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1"));
@@ -178,23 +189,6 @@ class HttpGetterTest {
Assertions.assertEquals(2, calls.get()); Assertions.assertEquals(2, calls.get());
} }
@Test
void supportsCompression() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
String acceptEncodingHeader = req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
if (!acceptEncodingHeader.contains("deflate")) {
throw new Exception("deflate should be in the Accept-Encoding header");
}
if (!acceptEncodingHeader.contains("gzip")) {
throw new Exception("gzip should be in the Accept-Encoding header");
}
return HttpResponse.response().withBody("ok");
});
Assertions.assertDoesNotThrow(() -> getter.getBinary(this.feedUrl));
}
@Test @Test
void largeFeedWithContentLengthHeader() { void largeFeedWithContentLengthHeader() {
byte[] bytes = new byte[100000]; byte[] bytes = new byte[100000];
@@ -226,4 +220,46 @@ class HttpGetterTest {
Assertions.assertEquals("ok", new String(result.getContent())); Assertions.assertEquals("ok", new String(result.getContent()));
} }
@Nested
class Compression {
@Test
void deflate() throws IOException, NotModifiedException {
supportsCompression("deflate", DeflaterOutputStream::new);
}
@Test
void gzip() throws IOException, NotModifiedException {
supportsCompression("gzip", GZIPOutputStream::new);
}
void supportsCompression(String encoding, CompressionOutputStreamFunction compressionOutputStreamFunction)
throws IOException, NotModifiedException {
String body = "my body";
HttpGetterTest.this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
String acceptEncodingHeader = req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
if (!Set.of(acceptEncodingHeader.split(", ")).contains(encoding)) {
throw new Exception(encoding + " should be in the Accept-Encoding header");
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (OutputStream compressionOutputStream = compressionOutputStreamFunction.apply(output)) {
compressionOutputStream.write(body.getBytes());
}
return HttpResponse.response().withBody(output.toByteArray()).withHeader(HttpHeaders.CONTENT_ENCODING, encoding);
});
HttpResult result = getter.getBinary(HttpGetterTest.this.feedUrl);
Assertions.assertEquals(body, new String(result.getContent()));
}
@FunctionalInterface
public interface CompressionOutputStreamFunction {
OutputStream apply(OutputStream input) throws IOException;
}
}
} }

View File

@@ -13,6 +13,7 @@ import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
class FeedEntryFilteringServiceTest { class FeedEntryFilteringServiceTest {
private CommaFeedConfiguration config;
private FeedEntryFilteringService service; private FeedEntryFilteringService service;
@@ -20,8 +21,8 @@ class FeedEntryFilteringServiceTest {
@BeforeEach @BeforeEach
public void init() { public void init() {
CommaFeedConfiguration config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()).thenReturn(Duration.ofSeconds(2)); Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()).thenReturn(Duration.ofSeconds(30));
service = new FeedEntryFilteringService(config); service = new FeedEntryFilteringService(config);
@@ -69,6 +70,9 @@ class FeedEntryFilteringServiceTest {
@Test @Test
void cannotLoopForever() { void cannotLoopForever() {
Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()).thenReturn(Duration.ofMillis(200));
service = new FeedEntryFilteringService(config);
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("while(true) {}", entry)); Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("while(true) {}", entry));
} }

View File

@@ -0,0 +1,23 @@
package com.commafeed.integration;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class StaticFilesIT {
@ParameterizedTest
@ValueSource(strings = { "/", "/openapi.json", "/openapi.yaml" })
void servedWithoutCache(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "no-cache");
}
@ParameterizedTest
@ValueSource(strings = { "/favicon.ico" })
void servedWithCache(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=86400");
}
}

View File

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

View File

@@ -11,15 +11,23 @@
], ],
"packageRules": [ "packageRules": [
{ {
"description": "ignore our client because it's not published on maven central",
"matchManagers": "maven", "matchManagers": "maven",
"matchPackagePatterns": "commafeed-client", "matchPackagePatterns": "commafeed-client",
"enabled": false "enabled": false
}, },
{
"description": "io.quarkus.platform artifacts are released a week after io.quarkus artifacts",
"matchManagers": "maven",
"matchPackageNames": "io.quarkus:**",
"enabled": false
},
{ {
"matchManagers": "npm", "matchManagers": "npm",
"rangeStrategy": "bump" "rangeStrategy": "bump"
}, },
{ {
"description": "IBM Semeru Runtimes uses a custom versioning scheme",
"matchDatasources": "docker", "matchDatasources": "docker",
"matchPackageNames": "ibm-semeru-runtimes", "matchPackageNames": "ibm-semeru-runtimes",
"versioning": "regex:^open-(?<major>\\d+)?(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?([\\._+](?<build>(\\d\\.?)+))?(-(?<compatibility>.*))?$", "versioning": "regex:^open-(?<major>\\d+)?(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?([\\._+](?<build>(\\d\\.?)+))?(-(?<compatibility>.*))?$",