forked from Archives/Athou_commafeed
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cefc0f176 | |||
| 1cb346866a | |||
| 5cc8c736e7 | |||
| eb5614a03b | |||
|
|
fc76d7e609 | ||
|
|
1b24bf33ed | ||
|
|
58ff378735 | ||
|
|
1cee04a233 | ||
|
|
ac11a0efb8 | ||
|
|
f2ea1e3f7a | ||
|
|
153970c146 | ||
|
|
c21287e642 | ||
|
|
284942a82e | ||
|
|
e796916e73 | ||
| e2a1630adc | |||
|
|
004db1762c | ||
|
|
8e05ba8820 | ||
|
|
f4f386b5e5 | ||
|
|
ecbf8bec23 | ||
|
|
5d8c09ccda | ||
|
|
47c5c3d8a0 | ||
|
|
3ddce16d5b | ||
|
|
acefdc44d9 | ||
|
|
d05c5b9d7f | ||
|
|
5d1237d1f4 | ||
|
|
6bac8631ac | ||
|
|
2c803327ad | ||
|
|
451ae5bc51 | ||
|
|
f41ce8c878 | ||
|
|
177c54c813 | ||
|
|
3765e32fd4 | ||
|
|
aab7a16d18 | ||
|
|
f2463af63c | ||
|
|
212c2c3b56 | ||
|
|
3ab00b0cdd | ||
|
|
fad85e9299 | ||
|
|
3cd5e203e2 | ||
|
|
7b081fa870 | ||
|
|
5ce22051d4 | ||
|
|
1e59489fa5 | ||
|
|
1e691b1255 | ||
|
|
61e1bef63f | ||
|
|
46dbb78fbf | ||
|
|
99890707b3 | ||
|
|
f54c841fdc | ||
|
|
e2a6009ee9 | ||
|
|
a34dd15040 | ||
|
|
24c934003d | ||
|
|
5300e1a245 | ||
|
|
90381c670b | ||
|
|
23edea93db | ||
|
|
a51712e363 | ||
|
|
4310e979e1 | ||
|
|
f690b76d87 | ||
|
|
ac29594b67 | ||
|
|
93f535bb87 | ||
|
|
076eb3cf42 | ||
|
|
7b2e0fffbd | ||
|
|
8eaab0dbc3 | ||
|
|
eaa5bc896e | ||
|
|
42d1db5fc3 | ||
|
|
78c017ddaf | ||
|
|
231551d743 | ||
|
|
d0984eaba7 | ||
|
|
15854a72d1 | ||
|
|
7fd2bf0eda | ||
|
|
6a10a2167e | ||
|
|
1ba99d255c | ||
|
|
ff1c2947b6 | ||
|
|
a690d2e0db | ||
|
|
a3df327396 | ||
|
|
ede0016d8e | ||
|
|
c19b091795 | ||
|
|
9e62c8b9f3 | ||
|
|
9f30dc181c | ||
|
|
37fe44f860 | ||
|
|
68aaab8467 | ||
|
|
b951ed1fcd | ||
|
|
9bd9dc568a | ||
|
|
29e4356fee | ||
|
|
0ed31eaa99 | ||
|
|
1e9869b217 | ||
|
|
78cfc2c827 | ||
|
|
a6e5a0d125 | ||
|
|
ea13aecd27 | ||
|
|
d838e8f28f | ||
|
|
c9a7b9e17c | ||
|
|
8fe2d0bc0e | ||
|
|
71e2f1e1e6 | ||
|
|
1ce9d1b9b2 | ||
|
|
b3d6ae467f | ||
|
|
da8d720dc4 | ||
|
|
824c38f8ce | ||
|
|
b0579a70d8 | ||
|
|
f9fe2d0976 |
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -7,6 +7,7 @@ exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- enhancement
|
||||
- feature-request
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
# Setup
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # v1
|
||||
uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 # v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
@@ -67,14 +67,14 @@ jobs:
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload cross-platform app
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-jvm
|
||||
path: commafeed-server/target/commafeed-*.zip
|
||||
|
||||
- name: Upload native executable
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: commafeed-server/target/commafeed-*-runner*
|
||||
@@ -104,17 +104,17 @@ jobs:
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
|
||||
- name: Install required packages
|
||||
run: sudo apt-get install -y rename unzip
|
||||
|
||||
# Prepare artifacts
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: commafeed-${{ matrix.database }}-*
|
||||
path: ./artifacts
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
|
||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
|
||||
## build but don't push for PRs and renovate
|
||||
- name: Docker build - native
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
- name: Docker build - jvm
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
## build and push tag
|
||||
- name: Docker build and push tag - native
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push tag - jvm
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
|
||||
## build and push master
|
||||
- name: Docker build and push master - native
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
tags: athou/commafeed:master-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push master - jvm
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: commafeed-*
|
||||
path: ./artifacts
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1
|
||||
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
|
||||
2
.mvn/wrapper/maven-wrapper.properties
vendored
2
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -1,3 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
|
||||
|
||||
22
README-fork.md
Normal file
22
README-fork.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# `garrettmills/commafeed`
|
||||
|
||||
This is my personal fork of `Athou/commafeed` with some tweaks:
|
||||
|
||||
- "Infrequent" tab - like "All" but limits to blogs w/ an average post interval greater than a user-configurable number of days
|
||||
- User preference to disable the swipe-to-open-menu gesture on mobile
|
||||
|
||||
## Building
|
||||
|
||||
Use `gmfork-build-docker.sh` to build the JVM Docker image for `linux/amd64`:
|
||||
|
||||
You can use the `DB_VARIANT` env var to change which DB the image builds with. By default, it builds the `postgresql` variant.
|
||||
|
||||
```sh
|
||||
DOCKER_REGISTRY=myregistry.example.com DB_VARIANT=h2 ./gmfork-build-docker.sh
|
||||
```
|
||||
|
||||
To run locally:
|
||||
|
||||
```sh
|
||||
docker run -p 8082:8082 $DOCKER_REGISTRY/commafeed-fork:latest
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.2/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
|
||||
4040
commafeed-client/package-lock.json
generated
4040
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,19 +18,20 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@fontsource/open-sans": "^5.2.7",
|
||||
"@lingui/core": "^5.9.1",
|
||||
"@lingui/react": "^5.9.1",
|
||||
"@mantine/core": "^8.3.15",
|
||||
"@mantine/form": "^8.3.15",
|
||||
"@mantine/hooks": "^8.3.15",
|
||||
"@mantine/modals": "^8.3.15",
|
||||
"@mantine/notifications": "^8.3.15",
|
||||
"@mantine/spotlight": "^8.3.15",
|
||||
"@lingui/core": "^5.9.3",
|
||||
"@lingui/react": "^5.9.3",
|
||||
"@mantine/core": "^8.3.16",
|
||||
"@mantine/form": "^8.3.16",
|
||||
"@mantine/hooks": "^8.3.16",
|
||||
"@mantine/modals": "^8.3.16",
|
||||
"@mantine/notifications": "^8.3.16",
|
||||
"@mantine/spotlight": "^8.3.16",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-querybuilder/mantine": "^8.14.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"axios": "^1.13.5",
|
||||
"dayjs": "^1.11.19",
|
||||
"@rolldown/plugin-babel": "^0.2.2",
|
||||
"axios": "^1.13.6",
|
||||
"dayjs": "^1.11.20",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.1",
|
||||
"monaco-editor": "^0.55.1",
|
||||
@@ -40,11 +41,11 @@
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-querybuilder": "^8.14.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"style-to-object": "^1.0.14",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
@@ -53,10 +54,10 @@
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.2",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.9.1",
|
||||
"@lingui/cli": "^5.9.1",
|
||||
"@lingui/vite-plugin": "^5.9.1",
|
||||
"@biomejs/biome": "^2.4.7",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.9.3",
|
||||
"@lingui/cli": "^5.9.3",
|
||||
"@lingui/vite-plugin": "^5.9.3",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -66,16 +67,15 @@
|
||||
"@types/react-infinite-scroller": "^1.2.5",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycon": "^0.6.7",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^28.1.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"jsdom": "^29.0.0",
|
||||
"lint-staged": "^16.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-checker": "^0.12.0",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.0.18",
|
||||
"vitest": "^4.1.0",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
<properties>
|
||||
<!-- renovate: datasource=node-version depName=node -->
|
||||
<node.version>v24.13.1</node.version>
|
||||
<node.version>v24.14.0</node.version>
|
||||
<!-- renovate: datasource=npm depName=npm -->
|
||||
<npm.version>11.10.0</npm.version>
|
||||
<npm.version>11.11.1</npm.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
@@ -72,7 +72,7 @@
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy web interface to resources</id>
|
||||
@@ -94,4 +94,49 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<!-- This profile is used to kill the Biome process on Windows -->
|
||||
<!-- npm ci can fail if Biome is running (e.g., in the IDE) because it locks some files -->
|
||||
<profile>
|
||||
<id>kill-biome</id>
|
||||
<activation>
|
||||
<os>
|
||||
<family>Windows</family>
|
||||
</os>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>kill-biome</id>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>taskkill</executable>
|
||||
<arguments>
|
||||
<argument>/F</argument>
|
||||
<argument>/IM</argument>
|
||||
<argument>biome.exe</argument>
|
||||
</arguments>
|
||||
<successCodes>
|
||||
<successCode>0</successCode>
|
||||
<!-- taskkill returns 128 if the process is not found, which is fine in this case -->
|
||||
<successCode>128</successCode>
|
||||
</successCodes>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
@@ -18,6 +18,13 @@ const categories: Record<string, Omit<Category, "name">> = {
|
||||
feeds: [],
|
||||
position: 1,
|
||||
},
|
||||
infrequent: {
|
||||
id: "infrequent",
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
position: 2,
|
||||
},
|
||||
}
|
||||
|
||||
const sharing: {
|
||||
@@ -105,6 +112,7 @@ export const Constants = {
|
||||
tooltip: {
|
||||
delay: 500,
|
||||
},
|
||||
infrequentThresholdDaysDefault: 7,
|
||||
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
||||
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
|
||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
import { shallowEqual, type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
import { entriesSlice } from "@/app/entries/slice"
|
||||
import { redirectSlice } from "@/app/redirect/slice"
|
||||
import { serverSlice } from "@/app/server/slice"
|
||||
@@ -41,3 +41,4 @@ export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
export const useShallowEqualAppSelector: TypedUseSelectorHook<RootState> = selector => useSelector(selector, shallowEqual)
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Subscription {
|
||||
filterLegacy?: string
|
||||
pushNotificationsEnabled: boolean
|
||||
autoMarkAsReadAfterDays?: number
|
||||
averageEntryIntervalMs?: number
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
@@ -284,6 +285,8 @@ export interface Settings {
|
||||
unreadCountTitle: boolean
|
||||
unreadCountFavicon: boolean
|
||||
disablePullToRefresh: boolean
|
||||
disableMobileSwipe: boolean
|
||||
infrequentThresholdDays: number
|
||||
primaryColor?: string
|
||||
sharingSettings: SharingSettings
|
||||
pushNotificationSettings: PushNotificationSettings
|
||||
|
||||
@@ -4,9 +4,11 @@ import { createSlice, isAnyOf, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import type { LocalSettings, Settings, UserModel, ViewMode } from "@/app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeDisableMobileSwipe,
|
||||
changeDisablePullToRefresh,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeInfrequentThresholdDays,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMarkAllAsReadNavigateToUnread,
|
||||
@@ -141,6 +143,14 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.disablePullToRefresh = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeDisableMobileSwipe.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.disableMobileSwipe = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeInfrequentThresholdDays.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.infrequentThresholdDays = action.meta.arg
|
||||
})
|
||||
builder.addCase(changePrimaryColor.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.primaryColor = action.meta.arg
|
||||
@@ -171,6 +181,8 @@ export const userSlice = createSlice({
|
||||
changeUnreadCountTitle.fulfilled,
|
||||
changeUnreadCountFavicon.fulfilled,
|
||||
changeDisablePullToRefresh.fulfilled,
|
||||
changeDisableMobileSwipe.fulfilled,
|
||||
changeInfrequentThresholdDays.fulfilled,
|
||||
changePrimaryColor.fulfilled,
|
||||
changeSharingSetting.fulfilled,
|
||||
changePushNotificationSettings.fulfilled
|
||||
|
||||
@@ -131,6 +131,12 @@ export const changeDisablePullToRefresh = createAppAsyncThunk(
|
||||
}
|
||||
)
|
||||
|
||||
export const changeDisableMobileSwipe = createAppAsyncThunk("settings/disableMobileSwipe", (disableMobileSwipe: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, disableMobileSwipe })
|
||||
})
|
||||
|
||||
export const changePrimaryColor = createAppAsyncThunk("settings/primaryColor", (primaryColor: string, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
@@ -158,6 +164,15 @@ export const changeSharingSetting = createAppAsyncThunk(
|
||||
}
|
||||
)
|
||||
|
||||
export const changeInfrequentThresholdDays = createAppAsyncThunk(
|
||||
"settings/infrequentThresholdDays",
|
||||
(infrequentThresholdDays: number, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, infrequentThresholdDays })
|
||||
}
|
||||
)
|
||||
|
||||
export const changePushNotificationSettings = createAppAsyncThunk(
|
||||
"settings/pushNotificationSettings",
|
||||
(pushNotificationSettings: PushNotificationSettings, thunkApi) => {
|
||||
|
||||
@@ -26,20 +26,22 @@ export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
|
||||
return categories
|
||||
}
|
||||
|
||||
export function categoryUnreadCount(category?: TreeCategory): number {
|
||||
export function categoryUnreadCount(category?: TreeCategory, maxFrequencyThresholdMs?: number): number {
|
||||
if (!category) return 0
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
.flatMap(c => c.feeds)
|
||||
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
|
||||
.map(f => f.unread)
|
||||
.reduce((total, current) => total + current, 0)
|
||||
}
|
||||
|
||||
export function categoryHasNewEntries(category?: TreeCategory): boolean {
|
||||
export function categoryHasNewEntries(category?: TreeCategory, maxFrequencyThresholdMs?: number): boolean {
|
||||
if (!category) return false
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
.flatMap(c => c.feeds)
|
||||
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
|
||||
.some(f => f.hasNewEntries)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import type { IconDisplayMode, ScrollMode, SharingSettings } from "@/app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeDisableMobileSwipe,
|
||||
changeDisablePullToRefresh,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeInfrequentThresholdDays,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMarkAllAsReadNavigateToUnread,
|
||||
@@ -44,6 +46,8 @@ export function DisplaySettings() {
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const disablePullToRefresh = useAppSelector(state => state.user.settings?.disablePullToRefresh)
|
||||
const disableMobileSwipe = useAppSelector(state => state.user.settings?.disableMobileSwipe)
|
||||
const infrequentThresholdDays = useAppSelector(state => state.user.settings?.infrequentThresholdDays)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
|
||||
const { _ } = useLingui()
|
||||
@@ -143,6 +147,20 @@ export function DisplaySettings() {
|
||||
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>On mobile, disable swipe gesture to open the menu</Trans>}
|
||||
checked={disableMobileSwipe}
|
||||
onChange={async e => await dispatch(changeDisableMobileSwipe(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label={<Trans>Infrequent posts threshold (days)</Trans>}
|
||||
description={<Trans>Feeds posting less often than this (on average) will appear in the Infrequent view</Trans>}
|
||||
min={1}
|
||||
value={infrequentThresholdDays}
|
||||
onChange={async value => await dispatch(changeInfrequentThresholdDays(+value))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
|
||||
|
||||
<Switch
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Stack } from "@mantine/core"
|
||||
import React from "react"
|
||||
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||
import { TbChevronDown, TbChevronRight, TbClock, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||
import { Constants } from "@/app/constants"
|
||||
import {
|
||||
redirectToCategory,
|
||||
@@ -23,6 +23,7 @@ import { TreeSearch } from "./TreeSearch"
|
||||
|
||||
const allIcon = <TbInbox size={16} />
|
||||
const starredIcon = <TbStar size={16} />
|
||||
const infrequentIcon = <TbClock size={16} />
|
||||
const tagIcon = <TbTag size={16} />
|
||||
const expandedIcon = <TbChevronDown size={16} />
|
||||
const collapsedIcon = <TbChevronRight size={16} />
|
||||
@@ -34,6 +35,10 @@ export function Tree() {
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const infrequentThresholdDays = useAppSelector(
|
||||
state => state.user.settings?.infrequentThresholdDays ?? Constants.infrequentThresholdDaysDefault
|
||||
)
|
||||
const infrequentThresholdMs = infrequentThresholdDays * 24 * 3600 * 1000
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isFeedDisplayed = (feed: Subscription) => {
|
||||
@@ -115,6 +120,22 @@ export function Tree() {
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
const infrequentCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.infrequent.id}
|
||||
type="category"
|
||||
name={<Trans>Infrequent</Trans>}
|
||||
icon={infrequentIcon}
|
||||
unread={categoryUnreadCount(root, infrequentThresholdMs)}
|
||||
hasNewEntries={categoryHasNewEntries(root, infrequentThresholdMs)}
|
||||
selected={source.type === "category" && source.id === Constants.categories.infrequent.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
hasError={false}
|
||||
hasWarning={false}
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
|
||||
const categoryNode = (category: Category, level = 0) => {
|
||||
if (!isCategoryDisplayed(category)) return null
|
||||
@@ -197,6 +218,7 @@ export function Tree() {
|
||||
<Box className="cf-tree">
|
||||
{allCategoryNode()}
|
||||
{starredCategoryNode()}
|
||||
{infrequentCategoryNode()}
|
||||
{root.children.map(c => recursiveCategoryNode(c))}
|
||||
{root.feeds.map(f => feedNode(f))}
|
||||
{tags?.map(tag => tagNode(tag))}
|
||||
|
||||
@@ -6,7 +6,11 @@ const useStyles = tss.create(() => ({
|
||||
badge: {
|
||||
width: "3.2rem",
|
||||
// for some reason, mantine Badge has "cursor: 'default'"
|
||||
cursor: "pointer",
|
||||
cursor: "inherit",
|
||||
},
|
||||
indicator: {
|
||||
// ensure the indicator is not shown above the app header
|
||||
zIndex: 0,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -23,7 +27,15 @@ export function UnreadCount(
|
||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||
return (
|
||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||
<Indicator disabled={!props.showIndicator} size={4} offset={10} position="middle-start">
|
||||
<Indicator
|
||||
disabled={!props.showIndicator}
|
||||
size={4}
|
||||
offset={10}
|
||||
position="middle-start"
|
||||
classNames={{
|
||||
indicator: classes.indicator,
|
||||
}}
|
||||
>
|
||||
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
|
||||
{count}
|
||||
</Badge>
|
||||
|
||||
@@ -405,6 +405,10 @@ msgstr "Feed name"
|
||||
msgid "Feed URL"
|
||||
msgstr "Feed URL"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Feeds posting less often than this (on average) will appear in the Infrequent view"
|
||||
msgstr "Feeds posting less often than this (on average) will appear in the Infrequent view"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr "Fetch all my feeds now"
|
||||
@@ -502,6 +506,14 @@ msgstr "In expanded view, scrolling through entries mark them as read"
|
||||
msgid "Indigo"
|
||||
msgstr "Indigo"
|
||||
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Infrequent"
|
||||
msgstr "Infrequent"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Infrequent posts threshold (days)"
|
||||
msgstr "Infrequent posts threshold (days)"
|
||||
|
||||
#: src/pages/auth/InitialSetupPage.tsx
|
||||
msgid "Initial Setup"
|
||||
msgstr "Initial Setup"
|
||||
@@ -703,6 +715,10 @@ msgstr "On desktop"
|
||||
msgid "On mobile"
|
||||
msgstr "On mobile"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, disable swipe gesture to open the menu"
|
||||
msgstr "On mobile, disable swipe gesture to open the menu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "On mobile, show action buttons at the bottom of the screen"
|
||||
|
||||
@@ -8,9 +8,11 @@ import { Constants } from "@/app/constants"
|
||||
import type { EntrySourceType } from "@/app/entries/slice"
|
||||
import { loadEntries } from "@/app/entries/thunks"
|
||||
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { flattenCategoryTree } from "@/app/utils"
|
||||
import { useAppDispatch, useAppSelector, useShallowEqualAppSelector } from "@/app/store"
|
||||
import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "@/app/utils"
|
||||
import { FeedEntries } from "@/components/content/FeedEntries"
|
||||
import { UnreadCount } from "@/components/sidebar/UnreadCount"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
import { tss } from "@/tss"
|
||||
|
||||
function NoSubscriptionHelp() {
|
||||
@@ -33,6 +35,12 @@ const useStyles = tss.create(() => ({
|
||||
sourceWebsiteLink: {
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
overflow: "hidden",
|
||||
},
|
||||
titleText: {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -48,6 +56,33 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
|
||||
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
||||
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
|
||||
const hasMore = useAppSelector(state => state.entries.hasMore)
|
||||
const mobile = useMobile()
|
||||
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
|
||||
const { unreadCount, hasNewEntries } = useShallowEqualAppSelector(state => {
|
||||
const root = state.tree.rootCategory
|
||||
if (!root) return { unreadCount: 0, hasNewEntries: false }
|
||||
|
||||
if (props.sourceType === "category") {
|
||||
const category = id === Constants.categories.all.id ? root : flattenCategoryTree(root).find(c => c.id === id)
|
||||
return {
|
||||
unreadCount: categoryUnreadCount(category),
|
||||
hasNewEntries: categoryHasNewEntries(category),
|
||||
}
|
||||
}
|
||||
|
||||
if (props.sourceType === "feed") {
|
||||
const feed = flattenCategoryTree(root)
|
||||
.flatMap(c => c.feeds)
|
||||
.find(f => f.id === +id)
|
||||
return {
|
||||
unreadCount: feed?.unread ?? 0,
|
||||
hasNewEntries: !!feed?.hasNewEntries,
|
||||
}
|
||||
}
|
||||
|
||||
return { unreadCount: 0, hasNewEntries: false }
|
||||
})
|
||||
const showUnreadCount = mobile || !sidebarVisible
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
let title: React.ReactNode = sourceLabel
|
||||
@@ -89,16 +124,23 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
|
||||
return (
|
||||
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
|
||||
<Box mb={viewport.height * 0.7}>
|
||||
<Group gap="xl" className="cf-entries-title">
|
||||
<Group className="cf-entries-title" wrap="nowrap">
|
||||
{sourceWebsiteUrl && (
|
||||
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
|
||||
<Title order={3}>{title}</Title>
|
||||
<Title order={3} className={classes.titleText}>
|
||||
{title}
|
||||
</Title>
|
||||
</a>
|
||||
)}
|
||||
{!sourceWebsiteUrl && <Title order={3}>{title}</Title>}
|
||||
{!sourceWebsiteUrl && (
|
||||
<Title order={3} className={classes.titleText}>
|
||||
{title}
|
||||
</Title>
|
||||
)}
|
||||
<ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}>
|
||||
<TbEdit size={18} />
|
||||
</ActionIcon>
|
||||
{showUnreadCount && <UnreadCount unreadCount={unreadCount} showIndicator={hasNewEntries} />}
|
||||
</Group>
|
||||
|
||||
<FeedEntries />
|
||||
|
||||
@@ -79,6 +79,7 @@ export default function Layout(props: Readonly<LayoutProps>) {
|
||||
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
|
||||
const treeReloadInterval = useAppSelector(state => state.server.serverInfos?.treeReloadInterval)
|
||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||
const disableMobileSwipe = useAppSelector(state => state.user.settings?.disableMobileSwipe)
|
||||
const sidebarWidth = useAppSelector(state => state.user.localSettings.sidebarWidth)
|
||||
const headerInFooter = mobile && !isBrowserExtensionPopup && mobileFooter
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -164,6 +165,9 @@ export default function Layout(props: Readonly<LayoutProps>) {
|
||||
|
||||
const swipeHandlers = useSwipeable({
|
||||
onSwiping: e => {
|
||||
if (disableMobileSwipe) {
|
||||
return
|
||||
}
|
||||
const threshold = document.documentElement.clientWidth / 6
|
||||
if (e.absX > threshold) {
|
||||
dispatch(setMobileMenuOpen(e.dir === "Right"))
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { lingui } from "@lingui/vite-plugin"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import babel from "@rolldown/plugin-babel"
|
||||
import react, { reactCompilerPreset } from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
import checker from "vite-plugin-checker"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(() => ({
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: [
|
||||
// support for lingui macros
|
||||
// needs to be before the react compiler plugin
|
||||
"@lingui/babel-plugin-lingui-macro",
|
||||
// react compiler
|
||||
["babel-plugin-react-compiler", { target: "19" }],
|
||||
],
|
||||
},
|
||||
react(),
|
||||
babel({
|
||||
presets: [reactCompilerPreset()],
|
||||
plugins: ["@lingui/babel-plugin-lingui-macro"],
|
||||
}),
|
||||
lingui(),
|
||||
tsconfigPaths(),
|
||||
checker({
|
||||
typescript: true,
|
||||
biome: {
|
||||
@@ -43,22 +35,32 @@ export default defineConfig(() => ({
|
||||
"/logout": "http://localhost:8083",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 3500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: id => {
|
||||
// output mantine as its own chunk because it is quite large
|
||||
if (id.includes("@mantine")) {
|
||||
return "mantine"
|
||||
}
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
legacy: {
|
||||
// required for websocket-heartbeat-js
|
||||
inconsistentCjsInterop: true,
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: "./src/setupTests.ts",
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 4000,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
groups: [
|
||||
// output mantine as its own chunk because it is quite large
|
||||
{
|
||||
name: "mantine",
|
||||
test: "@mantine",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<packaging>quarkus</packaging>
|
||||
|
||||
<properties>
|
||||
<quarkus.version>3.31.4</quarkus.version>
|
||||
<quarkus.version>3.32.4</quarkus.version>
|
||||
<querydsl.version>7.1</querydsl.version>
|
||||
<rome.version>2.1.0</rome.version>
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- the quarkus bom declares a dependency on an old version of protobuf, we need to override it for cel-java -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>4.34.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -237,7 +243,7 @@
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>13.2.0</version>
|
||||
<version>13.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
@@ -266,7 +272,7 @@
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<version>3.4.0</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -302,7 +308,7 @@
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.42</version>
|
||||
<version>1.18.44</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -393,7 +399,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.cel</groupId>
|
||||
<artifactId>cel</artifactId>
|
||||
<version>0.11.1</version>
|
||||
<version>0.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.passay</groupId>
|
||||
@@ -428,7 +434,7 @@
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>78.2</version>
|
||||
<version>78.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.cssparser</groupId>
|
||||
@@ -448,7 +454,7 @@
|
||||
<dependency>
|
||||
<groupId>io.github.hakky54</groupId>
|
||||
<artifactId>ayza-for-apache5</artifactId>
|
||||
<version>10.0.3</version>
|
||||
<version>10.0.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.brotli</groupId>
|
||||
@@ -465,7 +471,7 @@
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.playwright</groupId>
|
||||
<artifactId>quarkus-playwright</artifactId>
|
||||
<version>2.3.2</version>
|
||||
<version>2.3.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:13.3@sha256:2c91e484d93f0830a7e05a2b9d92a7b102be7cab562198b984a84fdbc7806d91
|
||||
FROM debian:13.4@sha256:55a15a112b42be10bfc8092fcc40b6748dc236f7ef46a358d9392b339e9d60e8
|
||||
ARG TARGETARCH
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
@@ -10,7 +10,9 @@ import com.commafeed.security.password.PasswordConstraintValidator;
|
||||
import io.quarkus.runtime.ShutdownEvent;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
@RequiredArgsConstructor
|
||||
public class CommaFeedApplication {
|
||||
@@ -20,6 +22,8 @@ public class CommaFeedApplication {
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public void start(@Observes StartupEvent ev) {
|
||||
log.info("starting up...");
|
||||
|
||||
PasswordConstraintValidator.setMinimumPasswordLength(config.users().minimumPasswordLength());
|
||||
|
||||
feedRefreshEngine.start();
|
||||
@@ -27,6 +31,8 @@ public class CommaFeedApplication {
|
||||
}
|
||||
|
||||
public void stop(@Observes ShutdownEvent ev) {
|
||||
log.info("shutting down...");
|
||||
|
||||
feedRefreshEngine.stop();
|
||||
taskScheduler.stop();
|
||||
}
|
||||
|
||||
@@ -92,6 +92,12 @@ public interface CommaFeedConfiguration {
|
||||
@ConfigDocSection
|
||||
Websocket websocket();
|
||||
|
||||
/**
|
||||
* Duration to wait for the feed refresh engine and the task scheduler to stop when the application is shutting down.
|
||||
*/
|
||||
@WithDefault("2s")
|
||||
Duration shutdownTimeout();
|
||||
|
||||
interface HttpClient {
|
||||
/**
|
||||
* User-Agent string that will be used by the http client, leave empty for the default one.
|
||||
@@ -144,7 +150,7 @@ public interface CommaFeedConfiguration {
|
||||
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
|
||||
* resources.
|
||||
*
|
||||
* You may want to enable this if you host a public instance of CommaFeed with regisration open.
|
||||
* You may want to enable this if you host a public instance of CommaFeed with registrations open.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean blockLocalAddresses();
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.commafeed.backend.model.AbstractModel;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -41,12 +42,12 @@ public class FeedRefreshEngine {
|
||||
|
||||
private final BlockingDeque<Feed> queue;
|
||||
|
||||
private final ExecutorService feedProcessingLoopExecutor;
|
||||
private final ExecutorService refillLoopExecutor;
|
||||
private final ExecutorService refillExecutor;
|
||||
private final ThreadPoolExecutor workerExecutor;
|
||||
private final ThreadPoolExecutor databaseUpdaterExecutor;
|
||||
private final ThreadPoolExecutor notifierExecutor;
|
||||
private ExecutorService feedProcessingLoopExecutor;
|
||||
private ExecutorService refillLoopExecutor;
|
||||
private ThreadPoolExecutor refillExecutor;
|
||||
private ThreadPoolExecutor workerExecutor;
|
||||
private ThreadPoolExecutor databaseUpdaterExecutor;
|
||||
private ThreadPoolExecutor notifierExecutor;
|
||||
|
||||
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
|
||||
FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
|
||||
@@ -60,6 +61,15 @@ public class FeedRefreshEngine {
|
||||
|
||||
this.queue = new LinkedBlockingDeque<>();
|
||||
|
||||
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
|
||||
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) () -> workerExecutor.getActiveCount());
|
||||
metrics.register(MetricRegistry.name(getClass(), "updater", "active"),
|
||||
(Gauge<Integer>) () -> databaseUpdaterExecutor.getActiveCount());
|
||||
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) () -> notifierExecutor.getActiveCount());
|
||||
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size());
|
||||
}
|
||||
|
||||
private void createExecutors() {
|
||||
this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
|
||||
this.refillLoopExecutor = Executors.newSingleThreadExecutor();
|
||||
this.refillExecutor = newDiscardingSingleThreadExecutorService();
|
||||
@@ -67,15 +77,10 @@ public class FeedRefreshEngine {
|
||||
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
|
||||
this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
|
||||
config.pushNotifications().queueCapacity());
|
||||
|
||||
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
|
||||
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount);
|
||||
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount);
|
||||
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) notifierExecutor::getActiveCount);
|
||||
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size());
|
||||
}
|
||||
|
||||
public void start() {
|
||||
createExecutors();
|
||||
startFeedProcessingLoop();
|
||||
startRefillLoop();
|
||||
}
|
||||
@@ -197,12 +202,14 @@ public class FeedRefreshEngine {
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
this.feedProcessingLoopExecutor.shutdownNow();
|
||||
this.refillLoopExecutor.shutdownNow();
|
||||
this.refillExecutor.shutdownNow();
|
||||
this.workerExecutor.shutdownNow();
|
||||
this.databaseUpdaterExecutor.shutdownNow();
|
||||
this.notifierExecutor.shutdownNow();
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.feedProcessingLoopExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.refillLoopExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.databaseUpdaterExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
|
||||
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -98,7 +98,7 @@ public class Feed extends AbstractModel {
|
||||
private String etagHeader;
|
||||
|
||||
/**
|
||||
* average time between entries in the feed
|
||||
* average time between entries in the feed in milliseconds
|
||||
*/
|
||||
private Long averageEntryInterval;
|
||||
|
||||
|
||||
@@ -145,6 +145,9 @@ public class UserSettings extends AbstractModel {
|
||||
private boolean unreadCountTitle;
|
||||
private boolean unreadCountFavicon;
|
||||
private boolean disablePullToRefresh;
|
||||
private boolean disableMobileSwipe;
|
||||
|
||||
private int infrequentThresholdDays;
|
||||
|
||||
private boolean email;
|
||||
private boolean gmail;
|
||||
|
||||
@@ -54,10 +54,20 @@ public class DatabaseCleaningService {
|
||||
int deleted;
|
||||
long entriesTotal = 0;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping cleanup of feeds without subscriptions");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
|
||||
for (Feed feed : feeds) {
|
||||
long entriesDeleted;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping cleanup of feeds without subscriptions");
|
||||
return;
|
||||
}
|
||||
|
||||
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
|
||||
entriesDeletedMeter.mark(entriesDeleted);
|
||||
entriesTotal += entriesDeleted;
|
||||
@@ -76,6 +86,11 @@ public class DatabaseCleaningService {
|
||||
long total = 0;
|
||||
long deleted;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping cleanup of contents without entries");
|
||||
return;
|
||||
}
|
||||
|
||||
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize));
|
||||
total += deleted;
|
||||
log.debug("removed {} contents without entries", total);
|
||||
@@ -87,6 +102,11 @@ public class DatabaseCleaningService {
|
||||
log.info("cleaning entries exceeding feed capacity");
|
||||
long total = 0;
|
||||
while (true) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping cleanup of entries exceeding feed capacity");
|
||||
return;
|
||||
}
|
||||
|
||||
List<FeedCapacity> feeds = unitOfWork
|
||||
.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries));
|
||||
if (feeds.isEmpty()) {
|
||||
@@ -97,6 +117,11 @@ public class DatabaseCleaningService {
|
||||
long remaining = feed.capacity() - maxFeedCapacity;
|
||||
int deleted;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping cleanup of entries exceeding feed capacity");
|
||||
return;
|
||||
}
|
||||
|
||||
final long rem = remaining;
|
||||
deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries));
|
||||
entriesDeletedMeter.mark(deleted);
|
||||
@@ -114,6 +139,11 @@ public class DatabaseCleaningService {
|
||||
long total = 0;
|
||||
long deleted;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping cleanup of old entries");
|
||||
return;
|
||||
}
|
||||
|
||||
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries));
|
||||
entriesDeletedMeter.mark(deleted);
|
||||
total += deleted;
|
||||
@@ -127,6 +157,11 @@ public class DatabaseCleaningService {
|
||||
long total = 0;
|
||||
long deleted;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping cleanup of old read statuses");
|
||||
return;
|
||||
}
|
||||
|
||||
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
|
||||
total += deleted;
|
||||
log.debug("removed {} old read statuses", total);
|
||||
@@ -139,6 +174,11 @@ public class DatabaseCleaningService {
|
||||
long total = 0;
|
||||
long marked;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("interrupted, stopping marking entries as read");
|
||||
return;
|
||||
}
|
||||
|
||||
marked = unitOfWork.call(() -> feedEntryStatusDAO.autoMarkAsRead(batchSize));
|
||||
total += marked;
|
||||
log.debug("marked {} entries as read", total);
|
||||
|
||||
@@ -23,8 +23,8 @@ public abstract class ScheduledTask {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
};
|
||||
log.info("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(), getTimeUnit(),
|
||||
getInitialDelay(), getTimeUnit());
|
||||
log.debug("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(),
|
||||
getTimeUnit(), getInitialDelay(), getTimeUnit());
|
||||
executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,32 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import io.quarkus.arc.All;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import io.quarkus.arc.All;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class TaskScheduler {
|
||||
|
||||
private final List<ScheduledTask> tasks;
|
||||
private final ScheduledExecutorService executor;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public TaskScheduler(@All List<ScheduledTask> tasks) {
|
||||
private ScheduledExecutorService executor;
|
||||
|
||||
public TaskScheduler(@All List<ScheduledTask> tasks, CommaFeedConfiguration config) {
|
||||
this.tasks = tasks;
|
||||
this.executor = Executors.newScheduledThreadPool(tasks.size());
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
tasks.forEach(task -> task.register(executor));
|
||||
this.executor = Executors.newScheduledThreadPool(tasks.size());
|
||||
this.tasks.forEach(task -> task.register(executor));
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
executor.shutdownNow();
|
||||
MoreExecutors.shutdownAndAwaitTermination(executor, config.shutdownTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,12 @@ public class Settings implements Serializable {
|
||||
@Schema(description = "disable pull to refresh", required = true)
|
||||
private boolean disablePullToRefresh;
|
||||
|
||||
@Schema(description = "disable swipe gesture to open mobile menu", required = true)
|
||||
private boolean disableMobileSwipe;
|
||||
|
||||
@Schema(description = "threshold in days for the infrequent view", required = true)
|
||||
private int infrequentThresholdDays;
|
||||
|
||||
@Schema(description = "primary theme color to use in the UI")
|
||||
private String primaryColor;
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ public class Subscription implements Serializable {
|
||||
@Schema(description = "automatically mark entries as read after this many days (null to disable)")
|
||||
private Integer autoMarkAsReadAfterDays;
|
||||
|
||||
@Schema(description = "average time in milliseconds between entries in this feed, null if unknown")
|
||||
private Long averageEntryIntervalMs;
|
||||
|
||||
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
|
||||
FeedCategory category = subscription.getCategory();
|
||||
Feed feed = subscription.getFeed();
|
||||
@@ -93,6 +96,7 @@ public class Subscription implements Serializable {
|
||||
sub.setFilterLegacy(subscription.getFilterLegacy());
|
||||
sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
|
||||
sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays());
|
||||
sub.setAverageEntryIntervalMs(feed.getAverageEntryInterval());
|
||||
return sub;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,14 @@ import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
@@ -83,6 +85,7 @@ public class CategoryREST {
|
||||
|
||||
public static final String ALL = "all";
|
||||
public static final String STARRED = "starred";
|
||||
public static final String INFREQUENT = "infrequent";
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
@@ -90,6 +93,7 @@ public class CategoryREST {
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedSubscriptionService feedSubscriptionService;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final UriInfo uri;
|
||||
|
||||
@@ -139,11 +143,15 @@ public class CategoryREST {
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(id)) {
|
||||
if (ALL.equals(id) || INFREQUENT.equals(id)) {
|
||||
entries.setName(Optional.ofNullable(tag).orElse("All"));
|
||||
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
if (INFREQUENT.equals(id)) {
|
||||
entries.setName("Infrequent");
|
||||
removeFrequentSubscriptions(subs, user);
|
||||
}
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
|
||||
offset, limit + 1, order, true, tag, null, null);
|
||||
|
||||
@@ -244,9 +252,12 @@ public class CategoryREST {
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(req.getId())) {
|
||||
if (ALL.equals(req.getId()) || INFREQUENT.equals(req.getId())) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
if (INFREQUENT.equals(req.getId())) {
|
||||
removeFrequentSubscriptions(subs, user);
|
||||
}
|
||||
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
} else if (STARRED.equals(req.getId())) {
|
||||
feedEntryService.markStarredEntries(user, olderThan, insertedBefore);
|
||||
@@ -260,6 +271,17 @@ public class CategoryREST {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
private void removeFrequentSubscriptions(List<FeedSubscription> subs, User user) {
|
||||
UserSettings userSettings = userSettingsDAO.findByUser(user);
|
||||
int infrequentDays = userSettings != null && userSettings.getInfrequentThresholdDays() > 0
|
||||
? userSettings.getInfrequentThresholdDays()
|
||||
: 7;
|
||||
long infrequentThresholdMs = (long) infrequentDays * 24 * 3600 * 1000;
|
||||
|
||||
subs.removeIf(
|
||||
sub -> sub.getFeed().getAverageEntryInterval() == null || sub.getFeed().getAverageEntryInterval() < infrequentThresholdMs);
|
||||
}
|
||||
|
||||
private void removeExcludedSubscriptions(List<FeedSubscription> subs, List<Long> excludedIds) {
|
||||
if (CollectionUtils.isNotEmpty(excludedIds)) {
|
||||
subs.removeIf(sub -> excludedIds.contains(sub.getId()));
|
||||
|
||||
@@ -132,7 +132,9 @@ public class UserREST {
|
||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||
s.setDisableMobileSwipe(settings.isDisableMobileSwipe());
|
||||
s.setPrimaryColor(settings.getPrimaryColor());
|
||||
s.setInfrequentThresholdDays(settings.getInfrequentThresholdDays());
|
||||
|
||||
if (settings.getPushNotifications() != null) {
|
||||
s.getPushNotificationSettings().setType(settings.getPushNotifications().getType());
|
||||
@@ -168,6 +170,8 @@ public class UserREST {
|
||||
s.setUnreadCountTitle(false);
|
||||
s.setUnreadCountFavicon(true);
|
||||
s.setDisablePullToRefresh(false);
|
||||
s.setDisableMobileSwipe(false);
|
||||
s.setInfrequentThresholdDays(7);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
@@ -204,7 +208,9 @@ public class UserREST {
|
||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||
s.setDisableMobileSwipe(settings.isDisableMobileSwipe());
|
||||
s.setPrimaryColor(settings.getPrimaryColor());
|
||||
s.setInfrequentThresholdDays(settings.getInfrequentThresholdDays());
|
||||
|
||||
PushNotificationUserSettings ps = new PushNotificationUserSettings();
|
||||
ps.setType(settings.getPushNotificationSettings().getType());
|
||||
|
||||
@@ -59,6 +59,7 @@ quarkus.native.additional-build-args=-H:PageSize=65536
|
||||
# test profile overrides
|
||||
%test.quarkus.log.category."org.mockserver".level=WARN
|
||||
%test.quarkus.log.category."liquibase".level=WARN
|
||||
%test.commafeed.shutdown-timeout=100ms
|
||||
%test.commafeed.users.create-demo-account=true
|
||||
%test.commafeed.users.allow-registrations=true
|
||||
%test.commafeed.password-recovery-enabled=true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="add-infrequent-days-threshold" author="athou">
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="infrequentThresholdDays" type="INT" valueNumeric="7">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="add-disable-mobile-swipe" author="athou">
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="disableMobileSwipe" type="BOOLEAN" valueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -38,5 +38,6 @@
|
||||
<include file="changelogs/db.changelog-5.11.xml" />
|
||||
<include file="changelogs/db.changelog-5.12.xml" />
|
||||
<include file="changelogs/db.changelog-7.0.xml" />
|
||||
<include file="changelogs/db.changelog-gmfork.xml" />
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -6,6 +6,8 @@ import jakarta.persistence.EntityManager;
|
||||
import org.hibernate.Session;
|
||||
import org.kohsuke.MetaInfServices;
|
||||
|
||||
import io.quarkus.runtime.ShutdownEvent;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
|
||||
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
|
||||
|
||||
@@ -17,12 +19,17 @@ public class DatabaseReset implements QuarkusTestBeforeEachCallback {
|
||||
|
||||
@Override
|
||||
public void beforeEach(QuarkusTestMethodContext context) {
|
||||
CDI.current()
|
||||
.select(EntityManager.class)
|
||||
.get()
|
||||
.unwrap(Session.class)
|
||||
.getSessionFactory()
|
||||
.getSchemaManager()
|
||||
.truncateMappedObjects();
|
||||
// stop the application to make sure that there are no active transactions when we truncate the tables
|
||||
getBean(CommaFeedApplication.class).stop(new ShutdownEvent());
|
||||
|
||||
// truncate all tables so that we have a clean slate for the next test
|
||||
getBean(EntityManager.class).unwrap(Session.class).getSessionFactory().getSchemaManager().truncateMappedObjects();
|
||||
|
||||
// restart the application
|
||||
getBean(CommaFeedApplication.class).start(new StartupEvent());
|
||||
}
|
||||
|
||||
private static <T> T getBean(Class<T> clazz) {
|
||||
return CDI.current().select(clazz).get();
|
||||
}
|
||||
}
|
||||
|
||||
34
gmfork-build-docker.sh
Executable file
34
gmfork-build-docker.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DB_VARIANT="${DB_VARIANT:-postgresql}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
ARTIFACTS_DIR="$REPO_ROOT/artifacts"
|
||||
|
||||
if [ -z "${DOCKER_REGISTRY:-}" ]; then
|
||||
echo "Error: DOCKER_REGISTRY is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build
|
||||
cd "$REPO_ROOT"
|
||||
./mvnw --batch-mode --no-transfer-progress install -P${DB_VARIANT} -DskipTests
|
||||
|
||||
# Prepare artifacts
|
||||
rm -rf "$ARTIFACTS_DIR"
|
||||
mkdir -p "$ARTIFACTS_DIR"
|
||||
|
||||
cp commafeed-server/target/commafeed-*-${DB_VARIANT}-jvm.zip "$ARTIFACTS_DIR/"
|
||||
unzip -q "$ARTIFACTS_DIR"/*-${DB_VARIANT}-jvm.zip -d "$ARTIFACTS_DIR/extracted-jvm-package"
|
||||
mv "$ARTIFACTS_DIR/extracted-jvm-package"/commafeed-* "$ARTIFACTS_DIR/extracted-jvm-package/quarkus-app"
|
||||
|
||||
# Build image
|
||||
docker build \
|
||||
--platform linux/amd64 \
|
||||
--file commafeed-server/src/main/docker/Dockerfile.jvm \
|
||||
--tag "$DOCKER_REGISTRY/commafeed-fork:latest" \
|
||||
.
|
||||
|
||||
rm -rf "$ARTIFACTS_DIR"
|
||||
|
||||
echo "Built: $DOCKER_REGISTRY/commafeed-fork:latest"
|
||||
Reference in New Issue
Block a user