forked from Archives/Athou_commafeed
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d83173dbd | ||
|
|
f13368cb96 | ||
|
|
ec7e97e1de | ||
|
|
d4c9bd1dd7 | ||
|
|
6bff657d4d | ||
|
|
613d286be1 | ||
|
|
fd48108f8b | ||
|
|
c3cbd18df9 | ||
|
|
6685057dae | ||
|
|
0dec0e3788 | ||
|
|
1a73dd4004 | ||
|
|
eae80a6450 | ||
|
|
21a32ce0eb | ||
|
|
325533c5d9 | ||
|
|
7d819022f6 | ||
|
|
dba944874b | ||
|
|
ce9c12ec92 | ||
|
|
22dfc5774f | ||
|
|
d59091ab2b | ||
|
|
f69146a6bf | ||
|
|
43cdf3db3b | ||
|
|
280a354228 | ||
|
|
573b0431f9 | ||
|
|
9878b60e97 | ||
|
|
964033c2a7 | ||
|
|
d2e45aca91 | ||
|
|
daa99a2efc | ||
|
|
e986e9999a | ||
|
|
98d302cb94 | ||
|
|
bf11c4a7e4 | ||
|
|
e1cab952f8 | ||
|
|
bc28d4de27 | ||
|
|
bb901564e3 | ||
|
|
93acc9ded1 | ||
|
|
9b1c6a371e | ||
|
|
82bf8cd807 | ||
|
|
c2f2780c3f | ||
|
|
08f71d1f6f | ||
|
|
f498088beb | ||
|
|
347b41cf35 | ||
|
|
61ae90ad28 | ||
|
|
9a42fbafb2 | ||
|
|
938f9e9434 | ||
|
|
9004e453c2 | ||
|
|
7d33542691 | ||
|
|
c99348862c | ||
|
|
ac86db3966 | ||
|
|
e368810731 | ||
|
|
edae2f5a61 | ||
|
|
ab17c6f44e | ||
|
|
59dbae4f66 | ||
|
|
d7956292df | ||
|
|
1075497559 | ||
|
|
2d99fa03d3 | ||
|
|
72b64b6f0d | ||
|
|
a2096d3622 | ||
|
|
c81f9fb7b1 | ||
|
|
cc7e9e21fb | ||
|
|
803d537e51 | ||
|
|
9a83e5b6ef | ||
|
|
4323da9007 | ||
|
|
30b9b24be4 | ||
|
|
b191b00003 | ||
|
|
7e5cdcba34 | ||
|
|
45b30ad333 | ||
|
|
7ca087b0a6 | ||
|
|
188e4594fd | ||
|
|
2da80ce7d8 | ||
|
|
d5820f9aa5 | ||
|
|
b1a0aae0a5 | ||
|
|
cdd4d4b063 | ||
|
|
21f675e80b | ||
|
|
380724d73e | ||
|
|
2d26c5dee3 | ||
|
|
29bcc5ccf5 | ||
|
|
91497ab45a | ||
|
|
be77968570 | ||
|
|
a42dacc48d | ||
|
|
cd06055246 | ||
|
|
62c1f25ffc | ||
|
|
415dc15d6c | ||
|
|
1d0c87c679 | ||
|
|
e51c486a04 | ||
|
|
73808c1a70 | ||
|
|
fbcc2ecd0f | ||
|
|
3997606774 | ||
|
|
b988b599d5 | ||
|
|
3e2ff2959d | ||
|
|
5714a63d27 | ||
|
|
12b18d1e04 | ||
|
|
232141cb56 | ||
|
|
c4334e5e6e | ||
|
|
ddf78f880b | ||
|
|
b3651f3fba | ||
|
|
24943b868c | ||
|
|
ef71a691ef | ||
|
|
01593d94eb | ||
|
|
b793cc66d1 | ||
|
|
3810dedf47 | ||
|
|
9115797dee | ||
|
|
232658b934 | ||
|
|
f99fe57695 | ||
|
|
ec89d41112 | ||
|
|
f6d26a77cc | ||
|
|
860852cc12 | ||
|
|
d06d76401c | ||
|
|
f5b04a783e | ||
|
|
964e470951 | ||
|
|
612f8722dd | ||
|
|
e118dc9b7f | ||
|
|
6e42cdaf2d | ||
|
|
5198792ca5 | ||
|
|
10a71213f3 | ||
|
|
a5d0979d9f | ||
|
|
d84225ab1c | ||
|
|
cd86947e64 | ||
|
|
f6b3114a91 | ||
|
|
cd50b6b058 | ||
|
|
b0c7ef18db | ||
|
|
24171faf86 | ||
|
|
941f14dd41 | ||
|
|
d46ef787db | ||
|
|
ec7447a38c | ||
|
|
2a3fc3ae15 | ||
|
|
ef25582bcb | ||
|
|
55bbb2542d | ||
|
|
8e94ac74a8 | ||
|
|
90ecb9253c | ||
|
|
6721842d98 | ||
|
|
8b487ec414 | ||
|
|
d6382861c3 | ||
|
|
2cdea99a69 | ||
|
|
b1ae1c8afd | ||
|
|
c09cd0c717 | ||
|
|
f50e0ae272 | ||
|
|
b99b91a2a8 | ||
|
|
d9759de6f1 | ||
|
|
cf2b7f9e4f | ||
|
|
ee880c06ed | ||
|
|
bc2e13ef22 | ||
|
|
39ecfe2782 | ||
|
|
3295d82f69 | ||
|
|
1cd27a59e2 | ||
|
|
e1602edff1 | ||
|
|
ef8e61d6fc | ||
|
|
0057030442 | ||
|
|
6fabe46d6e | ||
|
|
37c58f2755 | ||
|
|
bb982c3caf | ||
|
|
7e4c3737a8 | ||
|
|
23596b5ac6 | ||
|
|
2fdeb7acd8 | ||
|
|
c62cac478c | ||
|
|
e9026e0371 | ||
|
|
7446d906ae | ||
|
|
62ad09ac93 | ||
|
|
01d1f920a8 | ||
|
|
057810470c | ||
|
|
5a6d6be8e5 | ||
|
|
c6c813a4ee | ||
|
|
ad5787a38b | ||
|
|
387ceabf30 | ||
|
|
ffe6962c36 | ||
|
|
6d599fc77d | ||
|
|
9fcff1342c | ||
|
|
f7dbc2e9aa | ||
|
|
468f2e4c76 | ||
|
|
883c9c79aa | ||
|
|
f171d05088 | ||
|
|
f85745fe40 | ||
|
|
5ad93bb3ba | ||
|
|
d80ed9d4dd | ||
|
|
69b5f5418a | ||
|
|
06aa37659c | ||
|
|
d5c98de839 | ||
|
|
920975059c | ||
|
|
7c6e4c3356 | ||
|
|
143971da5e | ||
|
|
8976e9c01a | ||
|
|
20c6355efd | ||
|
|
f86f38ef7a | ||
|
|
24311df551 | ||
|
|
d02aa78def | ||
|
|
b131020f46 | ||
|
|
4ab82782b0 | ||
|
|
6f9ebd5d78 | ||
|
|
7ebbf26369 | ||
|
|
dbc93f9928 | ||
|
|
ad6ebd7e4d | ||
|
|
ab86247c8c | ||
|
|
884516be28 | ||
|
|
c236b1adda | ||
|
|
222117dafe | ||
|
|
38cd27df57 | ||
|
|
5e07e74bb2 | ||
|
|
fe779e361f | ||
|
|
1a51799497 | ||
|
|
6ea926cdb0 | ||
|
|
439d61946a | ||
|
|
426c8d7dfb | ||
|
|
f1b51e8342 |
24
.github/dependabot.yml
vendored
24
.github/dependabot.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "maven"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 50
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/commafeed-client"
|
|
||||||
schedule:
|
|
||||||
interval: "monthly"
|
|
||||||
open-pull-requests-limit: 50
|
|
||||||
groups:
|
|
||||||
mantine:
|
|
||||||
patterns:
|
|
||||||
- "@mantine/*"
|
|
||||||
lingui:
|
|
||||||
patterns:
|
|
||||||
- "@lingui/*"
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
44
.github/workflows/build.yml
vendored
44
.github/workflows/build.yml
vendored
@@ -29,10 +29,34 @@ jobs:
|
|||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: "maven"
|
cache: "maven"
|
||||||
|
|
||||||
# Build
|
# Build & Test
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn --batch-mode --update-snapshots verify
|
run: mvn --batch-mode --no-transfer-progress install
|
||||||
|
env:
|
||||||
|
TEST_DATABASE: h2
|
||||||
|
|
||||||
|
- name: Run integration tests on PostgreSQL
|
||||||
|
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||||
|
env:
|
||||||
|
TEST_DATABASE: postgresql
|
||||||
|
|
||||||
|
- name: Run integration tests on MySQL
|
||||||
|
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||||
|
env:
|
||||||
|
TEST_DATABASE: mysql
|
||||||
|
|
||||||
|
- name: Run integration tests on MariaDB
|
||||||
|
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||||
|
env:
|
||||||
|
TEST_DATABASE: mariadb
|
||||||
|
|
||||||
|
- name: Run integration tests with Redis cache enabled
|
||||||
|
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||||
|
env:
|
||||||
|
TEST_DATABASE: h2
|
||||||
|
REDIS: true
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
- name: Upload JAR
|
- name: Upload JAR
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ matrix.java == '17' }}
|
if: ${{ matrix.java == '17' }}
|
||||||
@@ -40,6 +64,14 @@ jobs:
|
|||||||
name: commafeed.jar
|
name: commafeed.jar
|
||||||
path: commafeed-server/target/commafeed.jar
|
path: commafeed-server/target/commafeed.jar
|
||||||
|
|
||||||
|
- name: Upload Playwright artifacts
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-artifacts
|
||||||
|
path: |
|
||||||
|
**/target/playwright-artifacts/
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -49,23 +81,23 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Docker build and push tag
|
- name: Docker build and push tag
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
tags: |
|
tags: |
|
||||||
athou/commafeed:latest
|
athou/commafeed:latest
|
||||||
athou/commafeed:${{ github.ref_name }}
|
athou/commafeed:${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Docker build and push master
|
- name: Docker build and push master
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
tags: athou/commafeed:master
|
tags: athou/commafeed:master
|
||||||
|
|
||||||
# Create GitHub release after Docker image has been published
|
# Create GitHub release after Docker image has been published
|
||||||
|
|||||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Binary file not shown.
6
.mvn/wrapper/maven-wrapper.properties
vendored
6
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -6,7 +6,7 @@
|
|||||||
# "License"); you may not use this file except in compliance
|
# "License"); you may not use this file except in compliance
|
||||||
# with the License. You may obtain a copy of the License at
|
# with the License. You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
#
|
#
|
||||||
# Unless required by applicable law or agreed to in writing,
|
# Unless required by applicable law or agreed to in writing,
|
||||||
# software distributed under the License is distributed on an
|
# software distributed under the License is distributed on an
|
||||||
@@ -14,5 +14,5 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip
|
distributionType=only-script
|
||||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [4.5.0]
|
||||||
|
|
||||||
|
- significantly reduce the time needed to retrieve entries or mark them as read, especially when there are a lot of
|
||||||
|
entries (#1452)
|
||||||
|
- fix a race condition where a feed could be refreshed before it was created in the database
|
||||||
|
- fix an issue that could cause the websocket notification to contain the wrong number of unread entries when using
|
||||||
|
mysql/mariadb
|
||||||
|
- fix an error when trying to mark all starred entries as read
|
||||||
|
- remove the `onlyIds` parameter from REST endpoints since retrieving all the entries is now just as fast
|
||||||
|
- remove support for microsoft sqlserver because it's not covered with integration tests (please open an issue if you'd
|
||||||
|
like it back)
|
||||||
|
|
||||||
|
## [4.4.1]
|
||||||
|
|
||||||
|
- fix vertical scrolling issues with Safari (#1168)
|
||||||
|
- the default value for new users for the "star entry" button and the "open in new tab" button in the entry headers is
|
||||||
|
now "on desktop" instead of "always"
|
||||||
|
- the "keyboard shortcuts" help page now shows "Cmd" instead of "Ctrl" on macOS (#1389)
|
||||||
|
- remove a superfluous feed fetch when subscribing to a feed (#1431)
|
||||||
|
- the Docker image now uses Java 21
|
||||||
|
|
||||||
## [4.4.0]
|
## [4.4.0]
|
||||||
|
|
||||||
- add support for sharing using the browser native capabilities if available (#1255)
|
- add support for sharing using the browser native capabilities if available (#1255)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM eclipse-temurin:17-jre
|
FROM eclipse-temurin:21.0.3_9-jre
|
||||||
|
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
dist
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
vite.config.ts
|
|
||||||
|
|
||||||
# compiled linguijs locales
|
|
||||||
# they no longer exist but we keep this to avoid issues with people still having those files on disk
|
|
||||||
src/locales/**/*.ts
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"eslint:recommended",
|
|
||||||
"standard",
|
|
||||||
"love",
|
|
||||||
"plugin:@typescript-eslint/strict-type-checked",
|
|
||||||
"plugin:@typescript-eslint/stylistic-type-checked",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: "detect",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
files: [".eslintrc.{js,cjs}"],
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: "script",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
project: true,
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
plugins: ["react"],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/no-confusing-void-expression": ["error", { ignoreArrowShorthand: true }],
|
|
||||||
"@typescript-eslint/no-floating-promises": "off",
|
|
||||||
"@typescript-eslint/no-misused-promises": "off",
|
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": ["error", { ignoreConditionalTests: true }],
|
|
||||||
"@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }],
|
|
||||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
|
||||||
"react/jsx-curly-brace-presence": ["error", "never"],
|
|
||||||
"react/no-unescaped-entities": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react-hooks/exhaustive-deps": "error",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 140,
|
|
||||||
"semi": false,
|
|
||||||
"tabWidth": 4,
|
|
||||||
"arrowParens": "avoid",
|
|
||||||
"endOfLine": "auto",
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
||||||
19
commafeed-client/biome.json
Normal file
19
commafeed-client/biome.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 4,
|
||||||
|
"lineEnding": "lf",
|
||||||
|
"lineWidth": 140
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"trailingCommas": "es5",
|
||||||
|
"semicolons": "asNeeded",
|
||||||
|
"arrowParentheses": "asNeeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignore": ["dist", "node_modules", "target", "target-ide"]
|
||||||
|
}
|
||||||
|
}
|
||||||
7028
commafeed-client/package-lock.json
generated
7028
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,74 +10,70 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:ci": "vitest run",
|
"test:ci": "vitest run",
|
||||||
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
|
"lint": "biome check ./src",
|
||||||
|
"lint:fix": "biome check --write ./src",
|
||||||
"i18n:extract": "lingui extract --clean"
|
"i18n:extract": "lingui extract --clean"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@fontsource/open-sans": "^5.0.27",
|
"@fontsource/open-sans": "^5.0.28",
|
||||||
"@lingui/core": "^4.10.0",
|
"@lingui/core": "^4.11.2",
|
||||||
"@lingui/macro": "^4.10.0",
|
"@lingui/macro": "^4.11.2",
|
||||||
"@lingui/react": "^4.10.0",
|
"@lingui/react": "^4.11.2",
|
||||||
"@mantine/core": "^7.8.0",
|
"@mantine/core": "^7.11.1",
|
||||||
"@mantine/form": "^7.8.0",
|
"@mantine/form": "^7.11.1",
|
||||||
"@mantine/hooks": "^7.8.0",
|
"@mantine/hooks": "^7.11.1",
|
||||||
"@mantine/modals": "^7.8.0",
|
"@mantine/modals": "^7.11.1",
|
||||||
"@mantine/notifications": "^7.8.0",
|
"@mantine/notifications": "^7.11.1",
|
||||||
"@mantine/spotlight": "^7.8.0",
|
"@mantine/spotlight": "^7.11.1",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.6",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.7.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.11",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"interweave": "^13.1.0",
|
"interweave": "^13.1.0",
|
||||||
"monaco-editor": "^0.47.0",
|
"monaco-editor": "^0.50.0",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.3.1",
|
||||||
"react-async-hook": "^4.0.0",
|
"react-async-hook": "^4.0.0",
|
||||||
"react-contexify": "^6.0.0",
|
"react-contexify": "^6.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-device-detect": "^2.2.3",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
"react-draggable": "^4.4.6",
|
"react-draggable": "^4.4.6",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
"react-icons": "^5.0.1",
|
"react-helmet": "^6.1.0",
|
||||||
|
"react-icons": "^5.2.1",
|
||||||
"react-infinite-scroller": "^1.2.6",
|
"react-infinite-scroller": "^1.2.6",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.2",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.24.1",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.1",
|
||||||
"redoc": "^2.1.3",
|
"redoc": "^2.1.5",
|
||||||
"throttle-debounce": "^5.0.0",
|
"throttle-debounce": "^5.0.2",
|
||||||
"tinycon": "^0.6.8",
|
"tinycon": "^0.6.8",
|
||||||
"tss-react": "^4.9.6",
|
"tss-react": "^4.9.10",
|
||||||
"use-local-storage": "^3.0.0",
|
"use-local-storage": "^3.0.0",
|
||||||
|
"vite-plugin-biome": "^1.0.12",
|
||||||
"websocket-heartbeat-js": "^1.1.3"
|
"websocket-heartbeat-js": "^1.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lingui/cli": "^4.10.0",
|
"@biomejs/biome": "^1.8.3",
|
||||||
"@lingui/vite-plugin": "^4.10.0",
|
"@lingui/cli": "^4.11.2",
|
||||||
|
"@lingui/vite-plugin": "^4.11.2",
|
||||||
"@types/mousetrap": "^1.6.15",
|
"@types/mousetrap": "^1.6.15",
|
||||||
"@types/react": "^18.2.78",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/react-helmet": "^6.1.11",
|
||||||
"@types/react-infinite-scroller": "^1.2.5",
|
"@types/react-infinite-scroller": "^1.2.5",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@types/tinycon": "^0.6.5",
|
"@types/tinycon": "^0.6.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-config-love": "^47.0.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-config-standard": "^17.1.0",
|
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
|
||||||
"eslint-plugin-react": "^7.34.1",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"prettier": "^3.2.5",
|
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.5.3",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.3.3",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"vitest": "^1.5.0",
|
"vitest": "^1.6.0",
|
||||||
"vitest-mock-extended": "^1.3.1"
|
"vitest-mock-extended": "^1.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>4.4.0</version>
|
<version>4.5.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<name>CommaFeed Client</name>
|
<name>CommaFeed Client</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<!-- renovate: datasource=node-version depName=node -->
|
||||||
|
<node.version>v20.15.0</node.version>
|
||||||
|
<!-- renovate: datasource=npm depName=npm -->
|
||||||
|
<npm.version>10.8.1</npm.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
@@ -25,8 +33,8 @@
|
|||||||
</goals>
|
</goals>
|
||||||
<phase>compile</phase>
|
<phase>compile</phase>
|
||||||
<configuration>
|
<configuration>
|
||||||
<nodeVersion>v20.10.0</nodeVersion>
|
<nodeVersion>${node.version}</nodeVersion>
|
||||||
<npmVersion>10.2.5</npmVersion>
|
<npmVersion>${npm.version}</npmVersion>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
<execution>
|
<execution>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { i18n } from "@lingui/core"
|
import { i18n } from "@lingui/core"
|
||||||
import { I18nProvider } from "@lingui/react"
|
import { I18nProvider } from "@lingui/react"
|
||||||
import { MantineProvider } from "@mantine/core"
|
import { MantineProvider } from "@mantine/core"
|
||||||
import { useDidUpdate } from "@mantine/hooks"
|
|
||||||
import { ModalsProvider } from "@mantine/modals"
|
import { ModalsProvider } from "@mantine/modals"
|
||||||
import { Notifications } from "@mantine/notifications"
|
import { Notifications } from "@mantine/notifications"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
@@ -9,11 +8,13 @@ import { redirectTo } from "app/redirect/slice"
|
|||||||
import { reloadServerInfos } from "app/server/thunks"
|
import { reloadServerInfos } from "app/server/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { categoryUnreadCount } from "app/utils"
|
import { categoryUnreadCount } from "app/utils"
|
||||||
|
import { DisablePullToRefresh } from "components/DisablePullToRefresh"
|
||||||
import { ErrorBoundary } from "components/ErrorBoundary"
|
import { ErrorBoundary } from "components/ErrorBoundary"
|
||||||
import { Header } from "components/header/Header"
|
import { Header } from "components/header/Header"
|
||||||
import { Tree } from "components/sidebar/Tree"
|
import { Tree } from "components/sidebar/Tree"
|
||||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||||
import { useI18n } from "i18n"
|
import { useI18n } from "i18n"
|
||||||
|
import { WelcomePage } from "pages/WelcomePage"
|
||||||
import { AdminUsersPage } from "pages/admin/AdminUsersPage"
|
import { AdminUsersPage } from "pages/admin/AdminUsersPage"
|
||||||
import { MetricsPage } from "pages/admin/MetricsPage"
|
import { MetricsPage } from "pages/admin/MetricsPage"
|
||||||
import { AboutPage } from "pages/app/AboutPage"
|
import { AboutPage } from "pages/app/AboutPage"
|
||||||
@@ -28,9 +29,10 @@ import { TagDetailsPage } from "pages/app/TagDetailsPage"
|
|||||||
import { LoginPage } from "pages/auth/LoginPage"
|
import { LoginPage } from "pages/auth/LoginPage"
|
||||||
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
||||||
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
||||||
import { WelcomePage } from "pages/WelcomePage"
|
import React, { useEffect } from "react"
|
||||||
import React, { useEffect, useRef } from "react"
|
import { isSafari } from "react-device-detect"
|
||||||
import ReactGA from "react-ga4"
|
import ReactGA from "react-ga4"
|
||||||
|
import { Helmet } from "react-helmet"
|
||||||
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
||||||
import Tinycon from "tinycon"
|
import Tinycon from "tinycon"
|
||||||
|
|
||||||
@@ -166,38 +168,13 @@ function BrowserExtensionBadgeUnreadCountHandler() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomJs() {
|
function CustomCode() {
|
||||||
const scriptLoaded = useRef(false)
|
return (
|
||||||
|
<Helmet>
|
||||||
// useDidUpdate is used instead of useEffect because we want to skip the first render
|
<link rel="stylesheet" type="text/css" href="custom_css.css" />
|
||||||
// the first render is the render of react-router, the routes are actually loaded in a second render
|
<script type="text/javascript" src="custom_js.js" />
|
||||||
// we want the script to be executed when the first route is done loading
|
</Helmet>
|
||||||
useDidUpdate(() => {
|
)
|
||||||
if (scriptLoaded.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const script = document.createElement("script")
|
|
||||||
script.src = "custom_js.js"
|
|
||||||
script.async = true
|
|
||||||
document.body.appendChild(script)
|
|
||||||
|
|
||||||
scriptLoaded.current = true
|
|
||||||
})
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function CustomCss() {
|
|
||||||
useEffect(() => {
|
|
||||||
const link = document.createElement("link")
|
|
||||||
link.rel = "stylesheet"
|
|
||||||
link.type = "text/css"
|
|
||||||
link.href = "custom_css.css"
|
|
||||||
document.head.appendChild(link)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
@@ -217,8 +194,12 @@ export function App() {
|
|||||||
<GoogleAnalyticsHandler />
|
<GoogleAnalyticsHandler />
|
||||||
<RedirectHandler />
|
<RedirectHandler />
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
<CustomJs />
|
<CustomCode />
|
||||||
<CustomCss />
|
{/* disable pull-to-refresh as it messes with vertical scrolling
|
||||||
|
safari behaves weirdly when overscroll-behavior is set to none so we disable it only for other browsers
|
||||||
|
https://github.com/Athou/commafeed/issues/1168
|
||||||
|
*/}
|
||||||
|
{!isSafari && <DisablePullToRefresh />}
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</>
|
</>
|
||||||
</Providers>
|
</Providers>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||||
import { type AppDispatch, type RootState } from "app/store"
|
import type { AppDispatch, RootState } from "app/store"
|
||||||
|
|
||||||
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
||||||
state: RootState
|
state: RootState
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import axios, { type AxiosError } from "axios"
|
import axios, { type AxiosError } from "axios"
|
||||||
import {
|
import type {
|
||||||
type AddCategoryRequest,
|
AddCategoryRequest,
|
||||||
type AdminSaveUserRequest,
|
AdminSaveUserRequest,
|
||||||
type AuthenticationError,
|
AuthenticationError,
|
||||||
type Category,
|
Category,
|
||||||
type CategoryModificationRequest,
|
CategoryModificationRequest,
|
||||||
type CollapseRequest,
|
CollapseRequest,
|
||||||
type Entries,
|
Entries,
|
||||||
type FeedInfo,
|
FeedInfo,
|
||||||
type FeedInfoRequest,
|
FeedInfoRequest,
|
||||||
type FeedModificationRequest,
|
FeedModificationRequest,
|
||||||
type GetEntriesPaginatedRequest,
|
GetEntriesPaginatedRequest,
|
||||||
type IDRequest,
|
IDRequest,
|
||||||
type LoginRequest,
|
LoginRequest,
|
||||||
type MarkRequest,
|
MarkRequest,
|
||||||
type Metrics,
|
Metrics,
|
||||||
type MultipleMarkRequest,
|
MultipleMarkRequest,
|
||||||
type PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
type ProfileModificationRequest,
|
ProfileModificationRequest,
|
||||||
type RegistrationRequest,
|
RegistrationRequest,
|
||||||
type ServerInfo,
|
ServerInfo,
|
||||||
type Settings,
|
Settings,
|
||||||
type StarRequest,
|
StarRequest,
|
||||||
type SubscribeRequest,
|
SubscribeRequest,
|
||||||
type Subscription,
|
Subscription,
|
||||||
type TagRequest,
|
TagRequest,
|
||||||
type UserModel,
|
UserModel,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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, SiTwitter } from "react-icons/si"
|
||||||
import { type Category, type Entry, type SharingSettings } from "./types"
|
import type { Category, Entry, SharingSettings } from "./types"
|
||||||
|
|
||||||
const categories: Record<string, Category> = {
|
const categories: Record<string, Category> = {
|
||||||
all: {
|
all: {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit"
|
import { configureStore } from "@reduxjs/toolkit"
|
||||||
import { type client } from "app/client"
|
import type { client } from "app/client"
|
||||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
|
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
|
||||||
import { reducers, type RootState } from "app/store"
|
import { type RootState, reducers } from "app/store"
|
||||||
import { type Entries, type Entry } from "app/types"
|
import type { Entries, Entry } from "app/types"
|
||||||
import { type AxiosResponse } from "axios"
|
import type { AxiosResponse } from "axios"
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
import { mockReset } from "vitest-mock-extended"
|
import { mockReset } from "vitest-mock-extended"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks"
|
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
|
|
||||||
export type EntrySourceType = "category" | "feed" | "tag"
|
export type EntrySourceType = "category" | "feed" | "tag"
|
||||||
|
|
||||||
@@ -51,11 +51,9 @@ export const entriesSlice = createSlice({
|
|||||||
state.selectedEntryId = action.payload.id
|
state.selectedEntryId = action.payload.id
|
||||||
},
|
},
|
||||||
setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => {
|
setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => {
|
||||||
state.entries
|
for (const e of state.entries.filter(e => e.id === action.payload.entry.id)) {
|
||||||
.filter(e => e.id === action.payload.entry.id)
|
|
||||||
.forEach(e => {
|
|
||||||
e.expanded = action.payload.expanded
|
e.expanded = action.payload.expanded
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
|
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
|
||||||
state.scrollingToEntry = action.payload
|
state.scrollingToEntry = action.payload
|
||||||
@@ -66,32 +64,24 @@ export const entriesSlice = createSlice({
|
|||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
builder.addCase(markEntry.pending, (state, action) => {
|
builder.addCase(markEntry.pending, (state, action) => {
|
||||||
state.entries
|
for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) {
|
||||||
.filter(e => e.id === action.meta.arg.entry.id)
|
|
||||||
.forEach(e => {
|
|
||||||
e.read = action.meta.arg.read
|
e.read = action.meta.arg.read
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
builder.addCase(markMultipleEntries.pending, (state, action) => {
|
builder.addCase(markMultipleEntries.pending, (state, action) => {
|
||||||
state.entries
|
for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) {
|
||||||
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))
|
|
||||||
.forEach(e => {
|
|
||||||
e.read = action.meta.arg.read
|
e.read = action.meta.arg.read
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
builder.addCase(markAllEntries.pending, (state, action) => {
|
builder.addCase(markAllEntries.pending, (state, action) => {
|
||||||
state.entries
|
for (const e of state.entries.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))) {
|
||||||
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
|
|
||||||
.forEach(e => {
|
|
||||||
e.read = true
|
e.read = true
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
builder.addCase(starEntry.pending, (state, action) => {
|
builder.addCase(starEntry.pending, (state, action) => {
|
||||||
state.entries
|
for (const e of state.entries.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)) {
|
||||||
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
|
|
||||||
.forEach(e => {
|
|
||||||
e.starred = action.meta.arg.starred
|
e.starred = action.meta.arg.starred
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
builder.addCase(loadEntries.pending, (state, action) => {
|
builder.addCase(loadEntries.pending, (state, action) => {
|
||||||
state.source = action.meta.arg.source
|
state.source = action.meta.arg.source
|
||||||
@@ -122,11 +112,9 @@ export const entriesSlice = createSlice({
|
|||||||
state.loading = false
|
state.loading = false
|
||||||
})
|
})
|
||||||
builder.addCase(tagEntry.pending, (state, action) => {
|
builder.addCase(tagEntry.pending, (state, action) => {
|
||||||
state.entries
|
for (const e of state.entries.filter(e => +e.id === action.meta.arg.entryId)) {
|
||||||
.filter(e => +e.id === action.meta.arg.entryId)
|
|
||||||
.forEach(e => {
|
|
||||||
e.tags = action.meta.arg.tags
|
e.tags = action.meta.arg.tags
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createAppAsyncThunk } from "app/async-thunk"
|
import { createAppAsyncThunk } from "app/async-thunk"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { entriesSlice, type EntrySource, type EntrySourceType, setSearch } from "app/entries/slice"
|
import { type EntrySource, type EntrySourceType, entriesSlice, setSearch } from "app/entries/slice"
|
||||||
import type { RootState } from "app/store"
|
import type { RootState } from "app/store"
|
||||||
import { reloadTree } from "app/tree/thunks"
|
import { reloadTree } from "app/tree/thunks"
|
||||||
import type { Entry, MarkRequest, TagRequest } from "app/types"
|
import type { Entry, MarkRequest, TagRequest } from "app/types"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||||
|
|
||||||
interface RedirectState {
|
interface RedirectState {
|
||||||
to?: string
|
to?: string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||||
import { reloadServerInfos } from "app/server/thunks"
|
import { reloadServerInfos } from "app/server/thunks"
|
||||||
import { type ServerInfo } from "app/types"
|
import type { ServerInfo } from "app/types"
|
||||||
|
|
||||||
interface ServerState {
|
interface ServerState {
|
||||||
serverInfos?: ServerInfo
|
serverInfos?: ServerInfo
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||||
import { markEntry } from "app/entries/thunks"
|
import { markEntry } from "app/entries/thunks"
|
||||||
import { redirectTo } from "app/redirect/slice"
|
import { redirectTo } from "app/redirect/slice"
|
||||||
import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
|
import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
|
||||||
import { type Category } from "app/types"
|
import type { Category } from "app/types"
|
||||||
import { visitCategoryTree } from "app/utils"
|
import { visitCategoryTree } from "app/utils"
|
||||||
|
|
||||||
interface TreeState {
|
interface TreeState {
|
||||||
@@ -34,13 +34,11 @@ export const treeSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
if (!state.rootCategory) return
|
if (!state.rootCategory) return
|
||||||
visitCategoryTree(state.rootCategory, c =>
|
visitCategoryTree(state.rootCategory, c => {
|
||||||
c.feeds
|
for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) {
|
||||||
.filter(f => f.id === action.payload.feedId)
|
|
||||||
.forEach(f => {
|
|
||||||
f.unread += action.payload.amount
|
f.unread += action.payload.amount
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
@@ -55,13 +53,11 @@ export const treeSlice = createSlice({
|
|||||||
})
|
})
|
||||||
builder.addCase(markEntry.pending, (state, action) => {
|
builder.addCase(markEntry.pending, (state, action) => {
|
||||||
if (!state.rootCategory) return
|
if (!state.rootCategory) return
|
||||||
visitCategoryTree(state.rootCategory, c =>
|
visitCategoryTree(state.rootCategory, c => {
|
||||||
c.feeds
|
for (const f of c.feeds.filter(f => f.id === +action.meta.arg.entry.feedId)) {
|
||||||
.filter(f => f.id === +action.meta.arg.entry.feedId)
|
|
||||||
.forEach(f => {
|
|
||||||
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
|
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
builder.addCase(redirectTo, state => {
|
builder.addCase(redirectTo, state => {
|
||||||
state.mobileMenuOpen = false
|
state.mobileMenuOpen = false
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ export interface GetEntriesRequest {
|
|||||||
newerThan?: number
|
newerThan?: number
|
||||||
order?: ReadingOrder
|
order?: ReadingOrder
|
||||||
keywords?: string
|
keywords?: string
|
||||||
onlyIds?: boolean
|
|
||||||
excludedSubscriptionIds?: string
|
excludedSubscriptionIds?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/macro"
|
||||||
import { showNotification } from "@mantine/notifications"
|
import { showNotification } from "@mantine/notifications"
|
||||||
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||||
import { type Settings, type UserModel } from "app/types"
|
import type { Settings, UserModel } from "app/types"
|
||||||
import {
|
import {
|
||||||
changeCustomContextMenu,
|
changeCustomContextMenu,
|
||||||
changeExternalLinkIconDisplayMode,
|
changeExternalLinkIconDisplayMode,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { throttle } from "throttle-debounce"
|
import { throttle } from "throttle-debounce"
|
||||||
import { type Category } from "./types"
|
import type { Category } from "./types"
|
||||||
|
|
||||||
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
|
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
|
||||||
visitor(category)
|
visitor(category)
|
||||||
category.children.forEach(child => visitCategoryTree(child, visitor))
|
for (const child of category.children) {
|
||||||
|
visitCategoryTree(child, visitor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flattenCategoryTree(category: Category): Category[] {
|
export function flattenCategoryTree(category: Category): Category[] {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
||||||
import { type ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useActionButton } from "hooks/useActionButton"
|
import { useActionButton } from "hooks/useActionButton"
|
||||||
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
|
import { type MouseEventHandler, type ReactNode, forwardRef } from "react"
|
||||||
|
|
||||||
interface ActionButtonProps {
|
interface ActionButtonProps {
|
||||||
className?: string
|
className?: string
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
import { Alert as MantineAlert, Box } from "@mantine/core"
|
import { Box, Alert as MantineAlert } from "@mantine/core"
|
||||||
import { Fragment } from "react"
|
import { Fragment } from "react"
|
||||||
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
||||||
|
|
||||||
|
|||||||
15
commafeed-client/src/components/DisablePullToRefresh.tsx
Normal file
15
commafeed-client/src/components/DisablePullToRefresh.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Helmet } from "react-helmet"
|
||||||
|
|
||||||
|
export const DisablePullToRefresh = () => {
|
||||||
|
return (
|
||||||
|
<Helmet>
|
||||||
|
<style type="text/css">
|
||||||
|
{`
|
||||||
|
html, body {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</Helmet>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
|
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
|
||||||
|
import { useOs } from "@mantine/hooks"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
|
|
||||||
export function KeyboardShortcutsHelp() {
|
export function KeyboardShortcutsHelp() {
|
||||||
|
const isMacOS = useOs() === "macos"
|
||||||
return (
|
return (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Table striped highlightOnHover>
|
<Table striped highlightOnHover>
|
||||||
@@ -149,7 +151,7 @@ export function KeyboardShortcutsHelp() {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Kbd>
|
<Kbd>
|
||||||
<Trans>Ctrl</Trans>
|
<Trans>{isMacOS ? "Cmd" : "Ctrl"}</Trans>
|
||||||
</Kbd>
|
</Kbd>
|
||||||
<span> + </span>
|
<span> + </span>
|
||||||
<Kbd>K</Kbd>
|
<Kbd>K</Kbd>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
|
|||||||
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { type AdminSaveUserRequest, type UserModel } from "app/types"
|
import type { AdminSaveUserRequest, UserModel } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
import { TbDeviceFloppy } from "react-icons/tb"
|
import { TbDeviceFloppy } from "react-icons/tb"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Input, Textarea } from "@mantine/core"
|
import { Input, Textarea } from "@mantine/core"
|
||||||
import RichCodeEditor from "components/code/RichCodeEditor"
|
import RichCodeEditor from "components/code/RichCodeEditor"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
import { type ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
|
|
||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
description?: ReactNode
|
description?: ReactNode
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAsync } from "react-async-hook"
|
|||||||
const init = async () => {
|
const init = async () => {
|
||||||
window.MonacoEnvironment = {
|
window.MonacoEnvironment = {
|
||||||
async getWorker(_, label) {
|
async getWorker(_, label) {
|
||||||
let worker
|
let worker: typeof import("*?worker")
|
||||||
if (label === "css") {
|
if (label === "css") {
|
||||||
worker = await import("monaco-editor/esm/vs/language/css/css.worker?worker")
|
worker = await import("monaco-editor/esm/vs/language/css/css.worker?worker")
|
||||||
} else if (label === "javascript") {
|
} else if (label === "javascript") {
|
||||||
@@ -13,7 +13,6 @@ const init = async () => {
|
|||||||
} else {
|
} else {
|
||||||
worker = await import("monaco-editor/esm/vs/editor/editor.worker?worker")
|
worker = await import("monaco-editor/esm/vs/editor/editor.worker?worker")
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
return new worker.default()
|
return new worker.default()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TypographyStylesProvider } from "@mantine/core"
|
import { TypographyStylesProvider } from "@mantine/core"
|
||||||
import { type ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is used to provide basic styles to html typography elements.
|
* This component is used to provide basic styles to html typography elements.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Box, Mark } from "@mantine/core"
|
import { Box, Mark } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { calculatePlaceholderSize } from "app/utils"
|
import { calculatePlaceholderSize } from "app/utils"
|
||||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
|
||||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||||
|
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||||
import escapeStringRegexp from "escape-string-regexp"
|
import escapeStringRegexp from "escape-string-regexp"
|
||||||
import { type ChildrenNode, Interweave, Matcher, type MatchResponse, type Node, type TransformCallback } from "interweave"
|
import { type ChildrenNode, Interweave, type MatchResponse, Matcher, type Node, type TransformCallback } from "interweave"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ const transform: TransformCallback = node => {
|
|||||||
const title = node.getAttribute("title") ?? undefined
|
const title = node.getAttribute("title") ?? undefined
|
||||||
const nodeWidth = node.getAttribute("width")
|
const nodeWidth = node.getAttribute("width")
|
||||||
const nodeHeight = node.getAttribute("height")
|
const nodeHeight = node.getAttribute("height")
|
||||||
const width = nodeWidth ? parseInt(nodeWidth, 10) : undefined
|
const width = nodeWidth ? Number.parseInt(nodeWidth, 10) : undefined
|
||||||
const height = nodeHeight ? parseInt(nodeHeight, 10) : undefined
|
const height = nodeHeight ? Number.parseInt(nodeHeight, 10) : undefined
|
||||||
const placeholderSize = calculatePlaceholderSize({
|
const placeholderSize = calculatePlaceholderSize({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
|
||||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||||
|
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||||
|
|
||||||
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) {
|
export function Enclosure(props: {
|
||||||
|
enclosureType: string
|
||||||
|
enclosureUrl: string
|
||||||
|
}) {
|
||||||
const hasVideo = props.enclosureType.startsWith("video")
|
const hasVideo = props.enclosureType.startsWith("video")
|
||||||
const hasAudio = props.enclosureType.startsWith("audio")
|
const hasAudio = props.enclosureType.startsWith("audio")
|
||||||
const hasImage = props.enclosureType.startsWith("image")
|
const hasImage = props.enclosureType.startsWith("image")
|
||||||
@@ -9,11 +12,13 @@ export function Enclosure(props: { enclosureType: string; enclosureUrl: string }
|
|||||||
return (
|
return (
|
||||||
<BasicHtmlStyles>
|
<BasicHtmlStyles>
|
||||||
{hasVideo && (
|
{hasVideo && (
|
||||||
|
// biome-ignore lint/a11y/useMediaCaption: we don't have any captions for videos
|
||||||
<video controls width="100%">
|
<video controls width="100%">
|
||||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||||
</video>
|
</video>
|
||||||
)}
|
)}
|
||||||
{hasAudio && (
|
{hasAudio && (
|
||||||
|
// biome-ignore lint/a11y/useMediaCaption: we don't have any captions for audio
|
||||||
<audio controls>
|
<audio controls>
|
||||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||||
</audio>
|
</audio>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { openModal } from "@mantine/modals"
|
import { openModal } from "@mantine/modals"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { type ExpendableEntry } from "app/entries/slice"
|
import type { ExpendableEntry } from "app/entries/slice"
|
||||||
import {
|
import {
|
||||||
loadMoreEntries,
|
loadMoreEntries,
|
||||||
markAllEntries,
|
markAllEntries,
|
||||||
@@ -126,7 +126,7 @@ export function FeedEntries() {
|
|||||||
})
|
})
|
||||||
window.addEventListener("scroll", listener)
|
window.addEventListener("scroll", listener)
|
||||||
return () => window.removeEventListener("scroll", listener)
|
return () => window.removeEventListener("scroll", listener)
|
||||||
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
|
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||||
|
|
||||||
useMousetrap("r", async () => await dispatch(reloadEntries()))
|
useMousetrap("r", async () => await dispatch(reloadEntries()))
|
||||||
useMousetrap(
|
useMousetrap(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
|
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { type Entry, type ViewMode } from "app/types"
|
import type { Entry, ViewMode } from "app/types"
|
||||||
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
|
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
|
||||||
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
|
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
import { useViewMode } from "hooks/useViewMode"
|
import { useViewMode } from "hooks/useViewMode"
|
||||||
import React from "react"
|
import type React from "react"
|
||||||
import { useSwipeable } from "react-swipeable"
|
import { useSwipeable } from "react-swipeable"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
import { FeedEntryBody } from "./FeedEntryBody"
|
import { FeedEntryBody } from "./FeedEntryBody"
|
||||||
@@ -35,7 +35,7 @@ const useStyles = tss
|
|||||||
maxWidth?: number
|
maxWidth?: number
|
||||||
}>()
|
}>()
|
||||||
.create(({ theme, colorScheme, read, expanded, viewMode, rtl, showSelectionIndicator, maxWidth }) => {
|
.create(({ theme, colorScheme, read, expanded, viewMode, rtl, showSelectionIndicator, maxWidth }) => {
|
||||||
let backgroundColor
|
let backgroundColor: string
|
||||||
if (colorScheme === "dark") {
|
if (colorScheme === "dark") {
|
||||||
backgroundColor = read ? "inherit" : theme.colors.dark[5]
|
backgroundColor = read ? "inherit" : theme.colors.dark[5]
|
||||||
} else {
|
} else {
|
||||||
@@ -61,7 +61,7 @@ const useStyles = tss
|
|||||||
backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
|
backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
let paperBorderLeftColor
|
let paperBorderLeftColor = ""
|
||||||
if (showSelectionIndicator) {
|
if (showSelectionIndicator) {
|
||||||
const borderLeftColor = colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6]
|
const borderLeftColor = colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6]
|
||||||
paperBorderLeftColor = `${borderLeftColor} !important`
|
paperBorderLeftColor = `${borderLeftColor} !important`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
import { Content } from "./Content"
|
import { Content } from "./Content"
|
||||||
import { Enclosure } from "./Enclosure"
|
import { Enclosure } from "./Enclosure"
|
||||||
import { Media } from "./Media"
|
import { Media } from "./Media"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Constants } from "app/constants"
|
|||||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
||||||
import { redirectToFeed } from "app/redirect/thunks"
|
import { redirectToFeed } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
import { truncate } from "app/utils"
|
import { truncate } from "app/utils"
|
||||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||||
import { useColorScheme } from "hooks/useColorScheme"
|
import { useColorScheme } from "hooks/useColorScheme"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
||||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
|
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
import { ActionButton } from "components/ActionButton"
|
import { ActionButton } from "components/ActionButton"
|
||||||
import { useActionButton } from "hooks/useActionButton"
|
import { useActionButton } from "hooks/useActionButton"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { calculatePlaceholderSize } from "app/utils"
|
import { calculatePlaceholderSize } from "app/utils"
|
||||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
|
||||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||||
|
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||||
import { Content } from "./Content"
|
import { Content } from "./Content"
|
||||||
|
|
||||||
export interface MediaProps {
|
export interface MediaProps {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { Trans } from "@lingui/macro"
|
|||||||
import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core"
|
import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { type SharingSettings } from "app/types"
|
import type { SharingSettings } from "app/types"
|
||||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
import { type IconType } from "react-icons"
|
import type { IconType } from "react-icons"
|
||||||
import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb"
|
import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { reloadTree } from "app/tree/thunks"
|
import { reloadTree } from "app/tree/thunks"
|
||||||
import { type AddCategoryRequest } from "app/types"
|
import type { AddCategoryRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
import { TbFolderPlus } from "react-icons/tb"
|
import { TbFolderPlus } from "react-icons/tb"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/macro"
|
||||||
import { Select, type SelectProps } from "@mantine/core"
|
import { Select, type SelectProps } from "@mantine/core"
|
||||||
import { type ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { type Category } from "app/types"
|
import type { Category } from "app/types"
|
||||||
import { flattenCategoryTree } from "app/utils"
|
import { flattenCategoryTree } from "app/utils"
|
||||||
|
|
||||||
type CategorySelectProps = Partial<SelectProps> & {
|
type CategorySelectProps = Partial<SelectProps> & {
|
||||||
@@ -18,7 +18,8 @@ export function CategorySelect(props: CategorySelectProps) {
|
|||||||
map.set(c.id, c)
|
map.set(c.id, c)
|
||||||
return map
|
return map
|
||||||
}, new Map<string, Category>())
|
}, new Map<string, Category>())
|
||||||
const categoryLabel = (cat: Category) => {
|
const categoryLabel = (category: Category) => {
|
||||||
|
let cat = category
|
||||||
let label = cat.name
|
let label = cat.name
|
||||||
|
|
||||||
while (cat.parentId) {
|
while (cat.parentId) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||||
import { isNotEmpty, useForm } from "@mantine/form"
|
import { isNotEmpty, useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Constants } from "app/constants"
|
|||||||
import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks"
|
import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { reloadTree } from "app/tree/thunks"
|
import { reloadTree } from "app/tree/thunks"
|
||||||
import { type FeedInfoRequest, type SubscribeRequest } from "app/types"
|
import type { FeedInfoRequest, SubscribeRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Box, Text } from "@mantine/core"
|
import { Box, Text } from "@mantine/core"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||||
import { Star } from "components/content/header/Star"
|
import { Star } from "components/content/header/Star"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
|
||||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Box, Flex, Space, Text } from "@mantine/core"
|
import { Box, Flex, Space, Text } from "@mantine/core"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||||
import { Star } from "components/content/header/Star"
|
import { Star } from "components/content/header/Star"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Highlight } from "@mantine/core"
|
import { Highlight } from "@mantine/core"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
|
|
||||||
export interface FeedEntryTitleProps {
|
export interface FeedEntryTitleProps {
|
||||||
entry: Entry
|
entry: Entry
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ActionIcon, Anchor, Tooltip } from "@mantine/core"
|
|||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { markEntry } from "app/entries/thunks"
|
import { markEntry } from "app/entries/thunks"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { type Entry } from "app/types"
|
import type { Entry } from "app/types"
|
||||||
import { TbExternalLink } from "react-icons/tb"
|
import { TbExternalLink } from "react-icons/tb"
|
||||||
|
|
||||||
export function OpenExternalLink(props: { entry: Entry }) {
|
export function OpenExternalLink(props: { entry: Entry }) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
|
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { showNotification } from "@mantine/notifications"
|
|||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
|
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type ViewMode } from "app/types"
|
import type { ViewMode } from "app/types"
|
||||||
import { useViewMode } from "hooks/useViewMode"
|
import { useViewMode } from "hooks/useViewMode"
|
||||||
import { type ReactNode, useState } from "react"
|
import { type ReactNode, useState } from "react"
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type MetricGauge } from "app/types"
|
import type { MetricGauge } from "app/types"
|
||||||
|
|
||||||
interface MeterProps {
|
interface MeterProps {
|
||||||
gauge: MetricGauge
|
gauge: MetricGauge
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { type MetricMeter } from "app/types"
|
import type { MetricMeter } from "app/types"
|
||||||
|
|
||||||
interface MeterProps {
|
interface MeterProps {
|
||||||
meter: MetricMeter
|
meter: MetricMeter
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { type MetricTimer } from "app/types"
|
import type { MetricTimer } from "app/types"
|
||||||
|
|
||||||
interface MetricTimerProps {
|
interface MetricTimerProps {
|
||||||
timer: MetricTimer
|
timer: MetricTimer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
import React from "react"
|
import type React from "react"
|
||||||
|
|
||||||
export function OnDesktop(props: { children: React.ReactNode }) {
|
export function OnDesktop(props: { children: React.ReactNode }) {
|
||||||
const mobile = useMobile()
|
const mobile = useMobile()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
import React from "react"
|
import type React from "react"
|
||||||
|
|
||||||
export function OnMobile(props: { children: React.ReactNode }) {
|
export function OnMobile(props: { children: React.ReactNode }) {
|
||||||
const mobile = useMobile()
|
const mobile = useMobile()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||||
import { type ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type IconDisplayMode, type ScrollMode, type SharingSettings } from "app/types"
|
import type { IconDisplayMode, ScrollMode, SharingSettings } from "app/types"
|
||||||
import {
|
import {
|
||||||
changeCustomContextMenu,
|
changeCustomContextMenu,
|
||||||
changeExternalLinkIconDisplayMode,
|
changeExternalLinkIconDisplayMode,
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
changeStarIconDisplayMode,
|
changeStarIconDisplayMode,
|
||||||
} 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"
|
||||||
|
|
||||||
export function DisplaySettings() {
|
export function DisplaySettings() {
|
||||||
const language = useAppSelector(state => state.user.settings?.language)
|
const language = useAppSelector(state => state.user.settings?.language)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { openConfirmModal } from "@mantine/modals"
|
import { openConfirmModal } from "@mantine/modals"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
|
import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type ProfileModificationRequest } from "app/types"
|
import type { ProfileModificationRequest } from "app/types"
|
||||||
import { reloadProfile } from "app/user/thunks"
|
import { reloadProfile } from "app/user/thunks"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "app/redirect/thunks"
|
} from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { collapseTreeCategory } from "app/tree/thunks"
|
import { collapseTreeCategory } from "app/tree/thunks"
|
||||||
import { type Category, type Subscription } from "app/types"
|
import type { Category, Subscription } from "app/types"
|
||||||
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Box, Center } from "@mantine/core"
|
import { Box, Center } from "@mantine/core"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import React, { type ReactNode } from "react"
|
import type React from "react"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
import { UnreadCount } from "./UnreadCount"
|
import { UnreadCount } from "./UnreadCount"
|
||||||
|
|
||||||
interface TreeNodeProps {
|
interface TreeNodeProps {
|
||||||
id: string
|
id: string
|
||||||
name: ReactNode
|
name: React.ReactNode
|
||||||
icon: ReactNode
|
icon: React.ReactNode
|
||||||
unread: number
|
unread: number
|
||||||
selected: boolean
|
selected: boolean
|
||||||
expanded?: boolean
|
expanded?: boolean
|
||||||
@@ -27,7 +27,7 @@ const useStyles = tss
|
|||||||
let backgroundColor = "inherit"
|
let backgroundColor = "inherit"
|
||||||
if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]
|
if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]
|
||||||
|
|
||||||
let color
|
let color: string
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
color = theme.colors.red[6]
|
color = theme.colors.red[6]
|
||||||
} else if (colorScheme === "dark") {
|
} else if (colorScheme === "dark") {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Box, Center, Kbd, TextInput } from "@mantine/core"
|
import { Box, Center, Kbd, TextInput } from "@mantine/core"
|
||||||
import { Spotlight, spotlight, type SpotlightActionData } from "@mantine/spotlight"
|
import { useOs } from "@mantine/hooks"
|
||||||
|
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
||||||
import { redirectToFeed } from "app/redirect/thunks"
|
import { redirectToFeed } from "app/redirect/thunks"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { type Subscription } from "app/types"
|
import type { Subscription } from "app/types"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import { useMousetrap } from "hooks/useMousetrap"
|
import { useMousetrap } from "hooks/useMousetrap"
|
||||||
import { TbSearch } from "react-icons/tb"
|
import { TbSearch } from "react-icons/tb"
|
||||||
@@ -14,7 +15,7 @@ export interface TreeSearchProps {
|
|||||||
|
|
||||||
export function TreeSearch(props: TreeSearchProps) {
|
export function TreeSearch(props: TreeSearchProps) {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const isMacOS = useOs() === "macos"
|
||||||
const actions: SpotlightActionData[] = props.feeds
|
const actions: SpotlightActionData[] = props.feeds
|
||||||
.map(f => ({
|
.map(f => ({
|
||||||
id: `${f.id}`,
|
id: `${f.id}`,
|
||||||
@@ -27,7 +28,7 @@ export function TreeSearch(props: TreeSearchProps) {
|
|||||||
const searchIcon = <TbSearch size={18} />
|
const searchIcon = <TbSearch size={18} />
|
||||||
const rightSection = (
|
const rightSection = (
|
||||||
<Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
|
<Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
|
||||||
<Kbd>Ctrl</Kbd>
|
<Kbd>{isMacOS ? "Cmd" : "Ctrl"}</Kbd>
|
||||||
<Box mx={5}>+</Box>
|
<Box mx={5}>+</Box>
|
||||||
<Kbd>K</Kbd>
|
<Kbd>K</Kbd>
|
||||||
</Center>
|
</Center>
|
||||||
@@ -56,13 +57,13 @@ export function TreeSearch(props: TreeSearchProps) {
|
|||||||
<Spotlight
|
<Spotlight
|
||||||
actions={actions}
|
actions={actions}
|
||||||
limit={10}
|
limit={10}
|
||||||
shortcut="ctrl+k"
|
shortcut="mod+k"
|
||||||
searchProps={{
|
searchProps={{
|
||||||
leftSection: searchIcon,
|
leftSection: searchIcon,
|
||||||
placeholder: t`Search`,
|
placeholder: t`Search`,
|
||||||
}}
|
}}
|
||||||
nothingFound={<Trans>Nothing found</Trans>}
|
nothingFound={<Trans>Nothing found</Trans>}
|
||||||
></Spotlight>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ export const useBrowserExtension = () => {
|
|||||||
// monitor the attribute on the root element as it may change after the page was loaded
|
// monitor the attribute on the root element as it may change after the page was loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new MutationObserver(mutations => {
|
const observer = new MutationObserver(mutations => {
|
||||||
mutations.forEach(mutation => {
|
for (const mutation of mutations) {
|
||||||
if (mutation.type === "attributes") {
|
if (mutation.type === "attributes") {
|
||||||
const element = mutation.target as Element
|
const element = mutation.target as Element
|
||||||
const version = element.getAttribute("browser-extension-installed")
|
const version = element.getAttribute("browser-extension-installed")
|
||||||
if (version) setBrowserExtensionVersion(version)
|
if (version) setBrowserExtensionVersion(version)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
observer.observe(document.documentElement, {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type ViewMode } from "app/types"
|
import type { ViewMode } from "app/types"
|
||||||
import useLocalStorage from "use-local-storage"
|
import useLocalStorage from "use-local-storage"
|
||||||
|
|
||||||
export function useViewMode() {
|
export function useViewMode() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { i18n, type Messages } from "@lingui/core"
|
import { type Messages, i18n } from "@lingui/core"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ msgstr "输入您当前的密码以更改配置文件设置"
|
|||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Entry headers"
|
msgid "Entry headers"
|
||||||
msgstr ""
|
msgstr "条目头部"
|
||||||
|
|
||||||
#: src/components/Alert.tsx
|
#: src/components/Alert.tsx
|
||||||
msgid "Error"
|
msgid "Error"
|
||||||
@@ -578,7 +578,7 @@ msgstr "没有更多条目"
|
|||||||
|
|
||||||
#: src/components/content/ShareButtons.tsx
|
#: src/components/content/ShareButtons.tsx
|
||||||
msgid "No sharing options available."
|
msgid "No sharing options available."
|
||||||
msgstr ""
|
msgstr "没有可用的分享选项"
|
||||||
|
|
||||||
#: src/components/sidebar/TreeSearch.tsx
|
#: src/components/sidebar/TreeSearch.tsx
|
||||||
msgid "Nothing found"
|
msgid "Nothing found"
|
||||||
@@ -590,11 +590,11 @@ msgstr "最早的优先"
|
|||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "On desktop"
|
msgid "On desktop"
|
||||||
msgstr ""
|
msgstr "桌面端"
|
||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "On mobile"
|
msgid "On mobile"
|
||||||
msgstr ""
|
msgstr "移动端"
|
||||||
|
|
||||||
#: 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"
|
||||||
@@ -660,7 +660,7 @@ msgstr "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 "OPML 文件是必需的"
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "Order"
|
msgid "Order"
|
||||||
@@ -718,7 +718,7 @@ msgstr "此 CommaFeed 实例上的注册已关闭"
|
|||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "REST API"
|
msgid "REST API"
|
||||||
msgstr ""
|
msgstr "REST API"
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
@@ -804,7 +804,7 @@ msgstr "显示条目菜单(移动端)"
|
|||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Show external link icon"
|
msgid "Show external link icon"
|
||||||
msgstr ""
|
msgstr "显示外部链接图标"
|
||||||
|
|
||||||
#: 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"
|
||||||
@@ -820,7 +820,7 @@ msgstr "显示原生菜单(桌面端)"
|
|||||||
|
|
||||||
#: src/components/settings/DisplaySettings.tsx
|
#: src/components/settings/DisplaySettings.tsx
|
||||||
msgid "Show star icon"
|
msgid "Show star icon"
|
||||||
msgstr ""
|
msgstr "显示星标图标"
|
||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
html, body {
|
|
||||||
/* disable pull-to-refresh on mobile as it messes with vertical scrolling */
|
|
||||||
overscroll-behavior: none;
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ import "@mantine/core/styles.css"
|
|||||||
import "@mantine/notifications/styles.css"
|
import "@mantine/notifications/styles.css"
|
||||||
import "@mantine/spotlight/styles.css"
|
import "@mantine/spotlight/styles.css"
|
||||||
import "react-contexify/ReactContexify.css"
|
import "react-contexify/ReactContexify.css"
|
||||||
import "main.css"
|
|
||||||
import { App } from "App"
|
import { App } from "App"
|
||||||
import { store } from "app/store"
|
import { store } from "app/store"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { Trans } from "@lingui/macro"
|
|||||||
import { ActionIcon, Box, Code, Container, Group, Table, Text, Title, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Box, Code, Container, Group, Table, Text, Title, useMantineTheme } from "@mantine/core"
|
||||||
import { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
|
import { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { type UserModel } from "app/types"
|
import type { UserModel } from "app/types"
|
||||||
import { UserEdit } from "components/admin/UserEdit"
|
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { type ReactNode } from "react"
|
import { UserEdit } from "components/admin/UserEdit"
|
||||||
|
import type { ReactNode } from "react"
|
||||||
import { useAsync, useAsyncCallback } from "react-async-hook"
|
import { useAsync, useAsyncCallback } from "react-async-hook"
|
||||||
import { TbCheck, TbPencil, TbPlus, TbTrash, TbX } from "react-icons/tb"
|
import { TbCheck, TbPencil, TbPlus, TbTrash, TbX } from "react-icons/tb"
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Anchor, Box, Container, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
import { Anchor, Box, Container, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { redirectToApiDocumentation } from "app/redirect/thunks"
|
import { redirectToApiDocumentation } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { CategorySelect } from "components/content/add/CategorySelect"
|
|
||||||
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||||
|
import { CategorySelect } from "components/content/add/CategorySelect"
|
||||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||||
import React, { useState } from "react"
|
import type React from "react"
|
||||||
|
import { useState } from "react"
|
||||||
import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb"
|
import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import { Constants } from "app/constants"
|
|||||||
import { redirectToRootCategory, redirectToSelectedSource } from "app/redirect/thunks"
|
import { redirectToRootCategory, redirectToSelectedSource } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { reloadTree } from "app/tree/thunks"
|
import { reloadTree } from "app/tree/thunks"
|
||||||
import { type CategoryModificationRequest } from "app/types"
|
import type { CategoryModificationRequest } from "app/types"
|
||||||
import { flattenCategoryTree } from "app/utils"
|
import { flattenCategoryTree } from "app/utils"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { CategorySelect } from "components/content/add/CategorySelect"
|
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
|
import { CategorySelect } from "components/content/add/CategorySelect"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useAsync, useAsyncCallback } from "react-async-hook"
|
import { useAsync, useAsyncCallback } from "react-async-hook"
|
||||||
import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { client, errorToStrings } from "app/client"
|
|||||||
import { redirectToRootCategory, redirectToSelectedSource } from "app/redirect/thunks"
|
import { redirectToRootCategory, redirectToSelectedSource } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { reloadTree } from "app/tree/thunks"
|
import { reloadTree } from "app/tree/thunks"
|
||||||
import { type FeedModificationRequest } from "app/types"
|
import type { FeedModificationRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { CategorySelect } from "components/content/add/CategorySelect"
|
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
|
import { CategorySelect } from "components/content/add/CategorySelect"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useAsync, useAsyncCallback } from "react-async-hook"
|
import { useAsync, useAsyncCallback } from "react-async-hook"
|
||||||
import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
|
|||||||
import { ActionIcon, Box, Center, Divider, Group, Title, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Box, Center, Divider, Group, Title, useMantineTheme } from "@mantine/core"
|
||||||
import { useViewportSize } from "@mantine/hooks"
|
import { useViewportSize } from "@mantine/hooks"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { type EntrySourceType } from "app/entries/slice"
|
import type { EntrySourceType } from "app/entries/slice"
|
||||||
import { loadEntries } from "app/entries/thunks"
|
import { loadEntries } from "app/entries/thunks"
|
||||||
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "app/redirect/thunks"
|
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
@@ -62,8 +62,9 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: we subscribe to state.timestamp because we want to reload entries even if the props are the same
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
const promise = dispatch(
|
||||||
loadEntries({
|
loadEntries({
|
||||||
source: {
|
source: {
|
||||||
type: props.sourceType,
|
type: props.sourceType,
|
||||||
@@ -72,7 +73,8 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
|||||||
clearSearch: true,
|
clearSearch: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}, [dispatch, props.sourceType, id, location.state])
|
return () => promise.abort()
|
||||||
|
}, [dispatch, props.sourceType, id, location.state?.timestamp])
|
||||||
|
|
||||||
const noSubscriptions = rootCategory && flattenCategoryTree(rootCategory).every(c => c.feeds.length === 0)
|
const noSubscriptions = rootCategory && flattenCategoryTree(rootCategory).every(c => c.feeds.length === 0)
|
||||||
if (noSubscriptions) return <NoSubscriptionHelp />
|
if (noSubscriptions) return <NoSubscriptionHelp />
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function Layout(props: LayoutProps) {
|
|||||||
label={mobileMenuOpen ? <Trans>Close menu</Trans> : <Trans>Open menu</Trans>}
|
label={mobileMenuOpen ? <Trans>Close menu</Trans> : <Trans>Open menu</Trans>}
|
||||||
icon={mobileMenuOpen ? <TbX size={18} /> : <TbMenu2 size={18} />}
|
icon={mobileMenuOpen ? <TbX size={18} /> : <TbMenu2 size={18} />}
|
||||||
onClick={() => dispatch(setMobileMenuOpen(!mobileMenuOpen))}
|
onClick={() => dispatch(setMobileMenuOpen(!mobileMenuOpen))}
|
||||||
></ActionButton>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const addButton = (
|
const addButton = (
|
||||||
@@ -201,7 +201,7 @@ export default function Layout(props: LayoutProps) {
|
|||||||
width: "10px",
|
width: "10px",
|
||||||
cursor: "ew-resize",
|
cursor: "ew-resize",
|
||||||
}}
|
}}
|
||||||
></Box>
|
/>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
</OnDesktop>
|
</OnDesktop>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { redirectToRootCategory } from "app/redirect/thunks"
|
import { redirectToRootCategory } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type LoginRequest } from "app/types"
|
import type { LoginRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { PageTitle } from "pages/PageTitle"
|
import { PageTitle } from "pages/PageTitle"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
@@ -50,6 +50,7 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
size="md"
|
size="md"
|
||||||
required
|
required
|
||||||
|
autoCapitalize="off"
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label={<Trans>Password</Trans>}
|
label={<Trans>Password</Trans>}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Anchor, Box, Button, Center, Container, Group, Paper, Stack, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Center, Container, Group, Paper, Stack, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { type PasswordResetRequest } from "app/types"
|
import type { PasswordResetRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { PageTitle } from "pages/PageTitle"
|
import { PageTitle } from "pages/PageTitle"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { redirectToRootCategory } from "app/redirect/thunks"
|
import { redirectToRootCategory } from "app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type RegistrationRequest } from "app/types"
|
import type { RegistrationRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { PageTitle } from "pages/PageTitle"
|
import { PageTitle } from "pages/PageTitle"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { lingui } from "@lingui/vite-plugin"
|
|||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
import { visualizer } from "rollup-plugin-visualizer"
|
import { visualizer } from "rollup-plugin-visualizer"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
import eslint from "vite-plugin-eslint"
|
import biomePlugin from "vite-plugin-biome"
|
||||||
import tsconfigPaths from "vite-tsconfig-paths"
|
import tsconfigPaths from "vite-tsconfig-paths"
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
@@ -16,9 +16,12 @@ export default defineConfig(env => ({
|
|||||||
}),
|
}),
|
||||||
lingui(),
|
lingui(),
|
||||||
// https://github.com/vitest-dev/vitest/issues/4055#issuecomment-1732994672
|
// https://github.com/vitest-dev/vitest/issues/4055#issuecomment-1732994672
|
||||||
env.mode !== "test" && eslint(),
|
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
visualizer(),
|
visualizer(),
|
||||||
|
biomePlugin({
|
||||||
|
mode: "check",
|
||||||
|
failOnError: true,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
base: "./",
|
base: "./",
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
@@ -104,10 +104,6 @@ app:
|
|||||||
# for PostgreSQL
|
# for PostgreSQL
|
||||||
# driverClass is org.postgresql.Driver
|
# driverClass is org.postgresql.Driver
|
||||||
# url is jdbc:postgresql://localhost:5432/commafeed
|
# url is jdbc:postgresql://localhost:5432/commafeed
|
||||||
#
|
|
||||||
# for Microsoft SQL Server
|
|
||||||
# driverClass is net.sourceforge.jtds.jdbc.Driver
|
|
||||||
# url is jdbc:jtds:sqlserver://localhost:1433/commafeed;instance=<instanceName, remove if not needed>
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
driverClass: org.h2.Driver
|
driverClass: org.h2.Driver
|
||||||
|
|||||||
@@ -104,10 +104,6 @@ app:
|
|||||||
# for PostgreSQL
|
# for PostgreSQL
|
||||||
# driverClass is org.postgresql.Driver
|
# driverClass is org.postgresql.Driver
|
||||||
# url is jdbc:postgresql://localhost:5432/commafeed
|
# url is jdbc:postgresql://localhost:5432/commafeed
|
||||||
#
|
|
||||||
# for Microsoft SQL Server
|
|
||||||
# driverClass is net.sourceforge.jtds.jdbc.Driver
|
|
||||||
# url is jdbc:jtds:sqlserver://localhost:1433/commafeed;instance=<instanceName, remove if not needed>
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
driverClass: org.h2.Driver
|
driverClass: org.h2.Driver
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" />
|
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" />
|
||||||
</module>
|
</module>
|
||||||
<module name="ParameterName" />
|
<module name="ParameterName" />
|
||||||
|
<module name="PatternVariableName" />
|
||||||
|
<module name="RecordComponentName" />
|
||||||
<module name="StaticVariableName" />
|
<module name="StaticVariableName" />
|
||||||
<module name="TypeName" />
|
<module name="TypeName" />
|
||||||
|
|
||||||
@@ -90,6 +92,9 @@
|
|||||||
<module name="SimplifyBooleanReturn" />
|
<module name="SimplifyBooleanReturn" />
|
||||||
<module name="StringLiteralEquality" />
|
<module name="StringLiteralEquality" />
|
||||||
<module name="UnnecessaryParentheses" />
|
<module name="UnnecessaryParentheses" />
|
||||||
|
<module name="UnnecessarySemicolonAfterTypeMemberDeclaration" />
|
||||||
|
<module name="UnnecessarySemicolonInTryWithResources" />
|
||||||
|
<module name="UnusedLocalVariable" />
|
||||||
|
|
||||||
<!-- Checks for class design -->
|
<!-- Checks for class design -->
|
||||||
<!-- See http://checkstyle.sf.net/config_design.html -->
|
<!-- See http://checkstyle.sf.net/config_design.html -->
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
- MYSQL_ROOT_PASSWORD=root
|
- MYSQL_ROOT_PASSWORD=root
|
||||||
- MYSQL_DATABASE=commafeed
|
- MYSQL_DATABASE=commafeed
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- "3306:3306"
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
image: postgres
|
image: postgres
|
||||||
@@ -16,4 +16,4 @@ services:
|
|||||||
- POSTGRES_PASSWORD=root
|
- POSTGRES_PASSWORD=root
|
||||||
- POSTGRES_DB=commafeed
|
- POSTGRES_DB=commafeed
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- "5432:5432"
|
||||||
|
|||||||
@@ -6,15 +6,25 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>4.4.0</version>
|
<version>4.5.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-server</artifactId>
|
<artifactId>commafeed-server</artifactId>
|
||||||
<name>CommaFeed Server</name>
|
<name>CommaFeed Server</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<guice.version>7.0.0</guice.version>
|
<guice.version>7.0.0</guice.version>
|
||||||
<querydsl.version>5.1.0</querydsl.version>
|
<querydsl.version>6.5</querydsl.version>
|
||||||
<rome.version>2.1.0</rome.version>
|
<rome.version>2.1.0</rome.version>
|
||||||
|
|
||||||
|
<testcontainers.version>1.19.8</testcontainers.version>
|
||||||
|
<!-- renovate: datasource=docker depName=postgres -->
|
||||||
|
<postgresql.image.version>16.3</postgresql.image.version>
|
||||||
|
<!-- renovate: datasource=docker depName=mysql -->
|
||||||
|
<mysql.image.version>9.0.0</mysql.image.version>
|
||||||
|
<!-- renovate: datasource=docker depName=mariadb -->
|
||||||
|
<mariadb.image.version>11.4.2</mariadb.image.version>
|
||||||
|
<!-- renovate: datasource=docker depName=redis -->
|
||||||
|
<redis.image.version>7.2.5</redis.image.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -31,17 +41,30 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<finalName>commafeed</finalName>
|
<finalName>commafeed</finalName>
|
||||||
|
<testResources>
|
||||||
|
<testResource>
|
||||||
|
<directory>src/test/resources</directory>
|
||||||
|
<filtering>false</filtering>
|
||||||
|
</testResource>
|
||||||
|
<testResource>
|
||||||
|
<directory>src/test/resources</directory>
|
||||||
|
<includes>
|
||||||
|
<include>docker-images.properties</include>
|
||||||
|
</includes>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
</testResource>
|
||||||
|
</testResources>
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
<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.2.5</version>
|
<version>3.3.0</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
<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.2.5</version>
|
<version>3.3.0</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<goals>
|
<goals>
|
||||||
@@ -54,7 +77,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>io.github.git-commit-id</groupId>
|
<groupId>io.github.git-commit-id</groupId>
|
||||||
<artifactId>git-commit-id-maven-plugin</artifactId>
|
<artifactId>git-commit-id-maven-plugin</artifactId>
|
||||||
<version>8.0.2</version>
|
<version>9.0.1</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<goals>
|
<goals>
|
||||||
@@ -64,7 +87,8 @@
|
|||||||
</executions>
|
</executions>
|
||||||
<configuration>
|
<configuration>
|
||||||
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
||||||
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
|
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties
|
||||||
|
</generateGitPropertiesFilename>
|
||||||
<failOnNoGitDirectory>false</failOnNoGitDirectory>
|
<failOnNoGitDirectory>false</failOnNoGitDirectory>
|
||||||
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
|
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -72,7 +96,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
<version>3.5.2</version>
|
<version>3.6.0</version>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.kordamp.shade</groupId>
|
<groupId>org.kordamp.shade</groupId>
|
||||||
@@ -86,6 +110,7 @@
|
|||||||
<filter>
|
<filter>
|
||||||
<artifact>*:*</artifact>
|
<artifact>*:*</artifact>
|
||||||
<excludes>
|
<excludes>
|
||||||
|
<exclude>module-info.class</exclude>
|
||||||
<exclude>META-INF/*.SF</exclude>
|
<exclude>META-INF/*.SF</exclude>
|
||||||
<exclude>META-INF/*.DSA</exclude>
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
<exclude>META-INF/*.RSA</exclude>
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
@@ -121,7 +146,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.21</version>
|
<version>2.2.22</version>
|
||||||
<?m2e ignore?>
|
<?m2e ignore?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<outputPath>${project.build.directory}/classes/assets</outputPath>
|
<outputPath>${project.build.directory}/classes/assets</outputPath>
|
||||||
@@ -145,7 +170,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
<version>3.4.0</version>
|
<version>3.4.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<archive>
|
<archive>
|
||||||
<manifest>
|
<manifest>
|
||||||
@@ -157,7 +182,7 @@
|
|||||||
<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.3.1</version>
|
<version>3.4.0</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>validate</id>
|
<id>validate</id>
|
||||||
@@ -211,13 +236,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<version>4.4.0</version>
|
<version>4.5.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.32</version>
|
<version>1.18.34</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -280,37 +305,27 @@
|
|||||||
<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.21</version>
|
<version>2.2.22</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.querydsl</groupId>
|
<groupId>io.github.openfeign.querydsl</groupId>
|
||||||
<artifactId>querydsl-apt</artifactId>
|
<artifactId>querydsl-apt</artifactId>
|
||||||
<version>${querydsl.version}</version>
|
<version>${querydsl.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
<classifier>jakarta</classifier>
|
<classifier>jakarta</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.querydsl</groupId>
|
<groupId>io.github.openfeign.querydsl</groupId>
|
||||||
<artifactId>querydsl-jpa</artifactId>
|
<artifactId>querydsl-jpa</artifactId>
|
||||||
<version>${querydsl.version}</version>
|
<version>${querydsl.version}</version>
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>commons-io</groupId>
|
|
||||||
<artifactId>commons-io</artifactId>
|
|
||||||
<version>2.16.1</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-collections4</artifactId>
|
<artifactId>commons-collections4</artifactId>
|
||||||
<version>4.4</version>
|
<version>4.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>commons-codec</groupId>
|
|
||||||
<artifactId>commons-codec</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-math3</artifactId>
|
<artifactId>commons-math3</artifactId>
|
||||||
@@ -336,7 +351,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>5.1.2</version>
|
<version>5.1.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.sun.mail</groupId>
|
<groupId>com.sun.mail</groupId>
|
||||||
@@ -373,7 +388,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ibm.icu</groupId>
|
<groupId>com.ibm.icu</groupId>
|
||||||
<artifactId>icu4j</artifactId>
|
<artifactId>icu4j</artifactId>
|
||||||
<version>74.2</version>
|
<version>75.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.cssparser</groupId>
|
<groupId>net.sourceforge.cssparser</groupId>
|
||||||
@@ -397,13 +412,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.hakky54</groupId>
|
<groupId>io.github.hakky54</groupId>
|
||||||
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
||||||
<version>8.3.4</version>
|
<version>8.3.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.apis</groupId>
|
<groupId>com.google.apis</groupId>
|
||||||
<artifactId>google-api-services-youtube</artifactId>
|
<artifactId>google-api-services-youtube</artifactId>
|
||||||
<version>v3-rev20240310-2.0.0</version>
|
<version>v3-rev20240514-2.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -413,29 +428,24 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.manticore-projects.tools</groupId>
|
<groupId>com.manticore-projects.tools</groupId>
|
||||||
<artifactId>h2migrationtool</artifactId>
|
<artifactId>h2migrationtool</artifactId>
|
||||||
<version>1.4</version>
|
<version>1.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.mysql</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>mysql-connector-j</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
<version>8.3.0</version>
|
<version>9.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mariadb.jdbc</groupId>
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
<artifactId>mariadb-java-client</artifactId>
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
<version>3.3.3</version>
|
<version>3.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>42.7.3</version>
|
<version>42.7.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>net.sourceforge.jtds</groupId>
|
|
||||||
<artifactId>jtds</artifactId>
|
|
||||||
<version>1.3.1</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
@@ -477,9 +487,33 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.playwright</groupId>
|
<groupId>com.microsoft.playwright</groupId>
|
||||||
<artifactId>playwright</artifactId>
|
<artifactId>playwright</artifactId>
|
||||||
<version>1.43.0</version>
|
<version>1.45.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>testcontainers</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>mysql</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>mariadb</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
configureObjectMapper(bootstrap.getObjectMapper());
|
configureObjectMapper(bootstrap.getObjectMapper());
|
||||||
|
|
||||||
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized
|
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized
|
||||||
bootstrap.addBundle(new ConfiguredBundle<CommaFeedConfiguration>() {
|
bootstrap.addBundle(new ConfiguredBundle<>() {
|
||||||
@Override
|
@Override
|
||||||
public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
|
public void run(CommaFeedConfiguration config, Environment environment) {
|
||||||
DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
|
DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
|
||||||
String url = dataSourceFactory.getUrl();
|
String url = dataSourceFactory.getUrl();
|
||||||
if (isFileBasedH2(url)) {
|
if (isFileBasedH2(url)) {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.commafeed.backend;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class Digests {
|
||||||
|
|
||||||
|
public static String sha1Hex(byte[] input) {
|
||||||
|
return hashBytesToHex(Hashing.sha1(), input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String sha1Hex(String input) {
|
||||||
|
return hashBytesToHex(Hashing.sha1(), input.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String md5Hex(String input) {
|
||||||
|
return hashBytesToHex(Hashing.md5(), input.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hashBytesToHex(HashFunction function, byte[] input) {
|
||||||
|
return function.hashBytes(input).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.commafeed.backend;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List wrapper that sorts its elements in the order provided by given comparator and ensure a maximum capacity.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class FixedSizeSortedList<E> {
|
|
||||||
|
|
||||||
private final List<E> inner;
|
|
||||||
|
|
||||||
private final Comparator<? super E> comparator;
|
|
||||||
private final int capacity;
|
|
||||||
|
|
||||||
public FixedSizeSortedList(int capacity, Comparator<? super E> comparator) {
|
|
||||||
this.inner = new ArrayList<>(Math.max(0, capacity));
|
|
||||||
this.capacity = capacity < 0 ? Integer.MAX_VALUE : capacity;
|
|
||||||
this.comparator = comparator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(E e) {
|
|
||||||
int position = Math.abs(Collections.binarySearch(inner, e, comparator) + 1);
|
|
||||||
if (isFull()) {
|
|
||||||
if (position < inner.size()) {
|
|
||||||
inner.remove(inner.size() - 1);
|
|
||||||
inner.add(position, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
inner.add(position, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public E last() {
|
|
||||||
return inner.get(inner.size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFull() {
|
|
||||||
return inner.size() == capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<E> asList() {
|
|
||||||
return inner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,6 +35,7 @@ import jakarta.inject.Inject;
|
|||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.altindag.ssl.SSLFactory;
|
import nl.altindag.ssl.SSLFactory;
|
||||||
import nl.altindag.ssl.apache5.util.Apache5SslUtils;
|
import nl.altindag.ssl.apache5.util.Apache5SslUtils;
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@Slf4j
|
||||||
public class HttpGetter {
|
public class HttpGetter {
|
||||||
|
|
||||||
private final CloseableHttpClient client;
|
private final CloseableHttpClient client;
|
||||||
@@ -70,8 +72,9 @@ public class HttpGetter {
|
|||||||
* if the url hasn't changed since we asked for it last time
|
* if the url hasn't changed since we asked for it last time
|
||||||
*/
|
*/
|
||||||
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws IOException, NotModifiedException {
|
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws IOException, NotModifiedException {
|
||||||
long start = System.currentTimeMillis();
|
log.debug("fetching {}", url);
|
||||||
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
ClassicHttpRequest request = ClassicRequestBuilder.get(url).build();
|
ClassicHttpRequest request = ClassicRequestBuilder.get(url).build();
|
||||||
if (lastModified != null) {
|
if (lastModified != null) {
|
||||||
request.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
|
request.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package com.commafeed.backend.cache;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import com.commafeed.backend.Digests;
|
||||||
|
|
||||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
@@ -20,7 +19,7 @@ public abstract class CacheService {
|
|||||||
public abstract void setLastEntries(Feed feed, List<String> entries);
|
public abstract void setLastEntries(Feed feed, List<String> entries);
|
||||||
|
|
||||||
public String buildUniqueEntryKey(Entry entry) {
|
public String buildUniqueEntryKey(Entry entry) {
|
||||||
return DigestUtils.sha1Hex(entry.guid() + entry.url());
|
return Digests.sha1Hex(entry.guid() + entry.url());
|
||||||
}
|
}
|
||||||
|
|
||||||
// user categories
|
// user categories
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
||||||
|
|
||||||
private final QFeedCategory category = QFeedCategory.feedCategory;
|
private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedCategoryDAO(SessionFactory sessionFactory) {
|
public FeedCategoryDAO(SessionFactory sessionFactory) {
|
||||||
@@ -25,31 +25,31 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedCategory> findAll(User user) {
|
public List<FeedCategory> findAll(User user) {
|
||||||
return query().selectFrom(category).where(category.user.eq(user)).join(category.user, QUser.user).fetchJoin().fetch();
|
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user)).join(CATEGORY.user, QUser.user).fetchJoin().fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public FeedCategory findById(User user, Long id) {
|
public FeedCategory findById(User user, Long id) {
|
||||||
return query().selectFrom(category).where(category.user.eq(user), category.id.eq(id)).fetchOne();
|
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.id.eq(id)).fetchOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public FeedCategory findByName(User user, String name, FeedCategory parent) {
|
public FeedCategory findByName(User user, String name, FeedCategory parent) {
|
||||||
Predicate parentPredicate;
|
Predicate parentPredicate;
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
parentPredicate = category.parent.isNull();
|
parentPredicate = CATEGORY.parent.isNull();
|
||||||
} else {
|
} else {
|
||||||
parentPredicate = category.parent.eq(parent);
|
parentPredicate = CATEGORY.parent.eq(parent);
|
||||||
}
|
}
|
||||||
return query().selectFrom(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).fetchOne();
|
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.name.eq(name), parentPredicate).fetchOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedCategory> findByParent(User user, FeedCategory parent) {
|
public List<FeedCategory> findByParent(User user, FeedCategory parent) {
|
||||||
Predicate parentPredicate;
|
Predicate parentPredicate;
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
parentPredicate = category.parent.isNull();
|
parentPredicate = CATEGORY.parent.isNull();
|
||||||
} else {
|
} else {
|
||||||
parentPredicate = category.parent.eq(parent);
|
parentPredicate = CATEGORY.parent.eq(parent);
|
||||||
}
|
}
|
||||||
return query().selectFrom(category).where(category.user.eq(user), parentPredicate).fetch();
|
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), parentPredicate).fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
|
public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class FeedDAO extends GenericDAO<Feed> {
|
public class FeedDAO extends GenericDAO<Feed> {
|
||||||
|
|
||||||
private final QFeed feed = QFeed.feed;
|
private static final QFeed FEED = QFeed.feed;
|
||||||
private final QFeedSubscription subscription = QFeedSubscription.feedSubscription;
|
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedDAO(SessionFactory sessionFactory) {
|
public FeedDAO(SessionFactory sessionFactory) {
|
||||||
@@ -27,25 +27,25 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) {
|
public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) {
|
||||||
JPAQuery<Feed> query = query().selectFrom(feed).where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(Instant.now())));
|
JPAQuery<Feed> query = query().selectFrom(FEED).where(FEED.disabledUntil.isNull().or(FEED.disabledUntil.lt(Instant.now())));
|
||||||
if (lastLoginThreshold != null) {
|
if (lastLoginThreshold != null) {
|
||||||
query.where(JPAExpressions.selectOne()
|
query.where(JPAExpressions.selectOne()
|
||||||
.from(subscription)
|
.from(SUBSCRIPTION)
|
||||||
.join(subscription.user)
|
.join(SUBSCRIPTION.user)
|
||||||
.where(subscription.feed.id.eq(feed.id), subscription.user.lastLogin.gt(lastLoginThreshold))
|
.where(SUBSCRIPTION.feed.id.eq(FEED.id), SUBSCRIPTION.user.lastLogin.gt(lastLoginThreshold))
|
||||||
.exists());
|
.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();
|
return query.orderBy(FEED.disabledUntil.asc()).limit(count).fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDisabledUntil(List<Long> feedIds, Instant date) {
|
public void setDisabledUntil(List<Long> feedIds, Instant date) {
|
||||||
updateQuery(feed).set(feed.disabledUntil, date).where(feed.id.in(feedIds)).execute();
|
updateQuery(FEED).set(FEED.disabledUntil, date).where(FEED.id.in(feedIds)).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
|
public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
|
||||||
return query().selectFrom(feed)
|
return query().selectFrom(FEED)
|
||||||
.where(feed.normalizedUrlHash.eq(normalizedUrlHash))
|
.where(FEED.normalizedUrlHash.eq(normalizedUrlHash))
|
||||||
.fetch()
|
.fetch()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(f -> StringUtils.equals(normalizedUrl, f.getNormalizedUrl()))
|
.filter(f -> StringUtils.equals(normalizedUrl, f.getNormalizedUrl()))
|
||||||
@@ -55,6 +55,6 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
|
|
||||||
public List<Feed> findWithoutSubscriptions(int max) {
|
public List<Feed> findWithoutSubscriptions(int max) {
|
||||||
QFeedSubscription sub = QFeedSubscription.feedSubscription;
|
QFeedSubscription sub = QFeedSubscription.feedSubscription;
|
||||||
return query().selectFrom(feed).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(feed)).notExists()).limit(max).fetch();
|
return query().selectFrom(FEED).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(FEED)).notExists()).limit(max).fetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.commafeed.backend.model.FeedEntryContent;
|
|||||||
import com.commafeed.backend.model.QFeedEntry;
|
import com.commafeed.backend.model.QFeedEntry;
|
||||||
import com.commafeed.backend.model.QFeedEntryContent;
|
import com.commafeed.backend.model.QFeedEntryContent;
|
||||||
import com.querydsl.jpa.JPAExpressions;
|
import com.querydsl.jpa.JPAExpressions;
|
||||||
import com.querydsl.jpa.JPQLQuery;
|
import com.querydsl.jpa.JPQLSubQuery;
|
||||||
|
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
@@ -16,8 +16,8 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||||
|
|
||||||
private final QFeedEntryContent content = QFeedEntryContent.feedEntryContent;
|
private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
|
||||||
private final QFeedEntry entry = QFeedEntry.feedEntry;
|
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedEntryContentDAO(SessionFactory sessionFactory) {
|
public FeedEntryContentDAO(SessionFactory sessionFactory) {
|
||||||
@@ -25,13 +25,13 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
|
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
|
||||||
return query().select(content).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).fetch();
|
return query().select(CONTENT).from(CONTENT).where(CONTENT.contentHash.eq(contentHash), CONTENT.titleHash.eq(titleHash)).fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long deleteWithoutEntries(int max) {
|
public long deleteWithoutEntries(int max) {
|
||||||
JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id));
|
JPQLSubQuery<Integer> subQuery = JPAExpressions.selectOne().from(ENTRY).where(ENTRY.content.id.eq(CONTENT.id));
|
||||||
List<Long> ids = query().select(content.id).from(content).where(subQuery.notExists()).limit(max).fetch();
|
List<Long> ids = query().select(CONTENT.id).from(CONTENT).where(subQuery.notExists()).limit(max).fetch();
|
||||||
|
|
||||||
return deleteQuery(content).where(content.id.in(ids)).execute();
|
return deleteQuery(CONTENT).where(CONTENT.id.in(ids)).execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import lombok.Getter;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||||
|
|
||||||
private final QFeedEntry entry = QFeedEntry.feedEntry;
|
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedEntryDAO(SessionFactory sessionFactory) {
|
public FeedEntryDAO(SessionFactory sessionFactory) {
|
||||||
@@ -27,22 +27,22 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public FeedEntry findExisting(String guidHash, Feed feed) {
|
public FeedEntry findExisting(String guidHash, Feed feed) {
|
||||||
return query().select(entry).from(entry).where(entry.guidHash.eq(guidHash), entry.feed.eq(feed)).limit(1).fetchOne();
|
return query().select(ENTRY).from(ENTRY).where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed)).limit(1).fetchOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
|
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
|
||||||
NumberExpression<Long> count = entry.id.count();
|
NumberExpression<Long> count = ENTRY.id.count();
|
||||||
List<Tuple> tuples = query().select(entry.feed.id, count)
|
List<Tuple> tuples = query().select(ENTRY.feed.id, count)
|
||||||
.from(entry)
|
.from(ENTRY)
|
||||||
.groupBy(entry.feed)
|
.groupBy(ENTRY.feed)
|
||||||
.having(count.gt(maxCapacity))
|
.having(count.gt(maxCapacity))
|
||||||
.limit(max)
|
.limit(max)
|
||||||
.fetch();
|
.fetch();
|
||||||
return tuples.stream().map(t -> new FeedCapacity(t.get(entry.feed.id), t.get(count))).toList();
|
return tuples.stream().map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count))).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int delete(Long feedId, long max) {
|
public int delete(Long feedId, long max) {
|
||||||
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch();
|
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch();
|
||||||
return delete(list);
|
return delete(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
|||||||
* Delete entries older than a certain date
|
* Delete entries older than a certain date
|
||||||
*/
|
*/
|
||||||
public int deleteEntriesOlderThan(Instant olderThan, long max) {
|
public int deleteEntriesOlderThan(Instant olderThan, long max) {
|
||||||
List<FeedEntry> list = query().selectFrom(entry).where(entry.updated.lt(olderThan)).orderBy(entry.updated.asc()).limit(max).fetch();
|
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.updated.lt(olderThan)).orderBy(ENTRY.updated.asc()).limit(max).fetch();
|
||||||
return delete(list);
|
return delete(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
|||||||
* Delete the oldest entries of a feed
|
* Delete the oldest entries of a feed
|
||||||
*/
|
*/
|
||||||
public int deleteOldEntries(Long feedId, long max) {
|
public int deleteOldEntries(Long feedId, long max) {
|
||||||
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).fetch();
|
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.updated.asc()).limit(max).fetch();
|
||||||
return delete(list);
|
return delete(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,20 @@ package com.commafeed.backend.dao;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.apache.commons.lang3.builder.CompareToBuilder;
|
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.FixedSizeSortedList;
|
|
||||||
import com.commafeed.backend.feed.FeedEntryKeyword;
|
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||||
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
|
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryContent;
|
|
||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
import com.commafeed.backend.model.FeedEntryTag;
|
import com.commafeed.backend.model.FeedEntryTag;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.Models;
|
|
||||||
import com.commafeed.backend.model.QFeedEntry;
|
import com.commafeed.backend.model.QFeedEntry;
|
||||||
import com.commafeed.backend.model.QFeedEntryContent;
|
import com.commafeed.backend.model.QFeedEntryContent;
|
||||||
import com.commafeed.backend.model.QFeedEntryStatus;
|
import com.commafeed.backend.model.QFeedEntryStatus;
|
||||||
@@ -27,7 +24,6 @@ import com.commafeed.backend.model.User;
|
|||||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||||
import com.commafeed.frontend.model.UnreadCount;
|
import com.commafeed.frontend.model.UnreadCount;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Ordering;
|
|
||||||
import com.querydsl.core.BooleanBuilder;
|
import com.querydsl.core.BooleanBuilder;
|
||||||
import com.querydsl.core.Tuple;
|
import com.querydsl.core.Tuple;
|
||||||
import com.querydsl.jpa.impl.JPAQuery;
|
import com.querydsl.jpa.impl.JPAQuery;
|
||||||
@@ -38,39 +34,30 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||||
|
|
||||||
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = (o1, o2) -> {
|
private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
|
||||||
CompareToBuilder builder = new CompareToBuilder();
|
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
||||||
builder.append(o2.getEntryUpdated(), o1.getEntryUpdated());
|
private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
|
||||||
builder.append(o2.getId(), o1.getId());
|
private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
|
||||||
return builder.toComparison();
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse();
|
|
||||||
|
|
||||||
private final FeedEntryDAO feedEntryDAO;
|
|
||||||
private final FeedEntryTagDAO feedEntryTagDAO;
|
private final FeedEntryTagDAO feedEntryTagDAO;
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
private final QFeedEntryStatus status = QFeedEntryStatus.feedEntryStatus;
|
|
||||||
private final QFeedEntry entry = QFeedEntry.feedEntry;
|
|
||||||
private final QFeedEntryContent content = QFeedEntryContent.feedEntryContent;
|
|
||||||
private final QFeedEntryTag entryTag = QFeedEntryTag.feedEntryTag;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryDAO feedEntryDAO, FeedEntryTagDAO feedEntryTagDAO,
|
public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
|
||||||
CommaFeedConfiguration config) {
|
|
||||||
super(sessionFactory);
|
super(sessionFactory);
|
||||||
this.feedEntryDAO = feedEntryDAO;
|
|
||||||
this.feedEntryTagDAO = feedEntryTagDAO;
|
this.feedEntryTagDAO = feedEntryTagDAO;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
|
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
|
||||||
List<FeedEntryStatus> statuses = query().selectFrom(status).where(status.entry.eq(entry), status.subscription.eq(sub)).fetch();
|
List<FeedEntryStatus> statuses = query().selectFrom(STATUS).where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub)).fetch();
|
||||||
FeedEntryStatus status = Iterables.getFirst(statuses, null);
|
FeedEntryStatus status = Iterables.getFirst(statuses, null);
|
||||||
return handleStatus(user, status, sub, entry);
|
return handleStatus(user, status, sub, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates an artificial "unread" status if status is null
|
||||||
|
*/
|
||||||
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||||
@@ -84,101 +71,103 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FeedEntryStatus fetchTags(User user, FeedEntryStatus status) {
|
private void fetchTags(User user, List<FeedEntryStatus> statuses) {
|
||||||
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, status.getEntry());
|
Map<Long, List<FeedEntryTag>> tagsByEntryIds = feedEntryTagDAO.findByEntries(user,
|
||||||
status.setTags(tags);
|
statuses.stream().map(FeedEntryStatus::getEntry).toList());
|
||||||
return status;
|
for (FeedEntryStatus status : statuses) {
|
||||||
|
List<FeedEntryTag> tags = tagsByEntryIds.get(status.getEntry().getId());
|
||||||
|
status.setTags(tags == null ? List.of() : tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedEntryStatus> findStarred(User user, Instant newerThan, int offset, int limit, ReadingOrder order,
|
public List<FeedEntryStatus> findStarred(User user, Instant newerThan, int offset, int limit, ReadingOrder order,
|
||||||
boolean includeContent) {
|
boolean includeContent) {
|
||||||
JPAQuery<FeedEntryStatus> query = query().selectFrom(status).where(status.user.eq(user), status.starred.isTrue());
|
JPAQuery<FeedEntryStatus> query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
|
||||||
|
if (includeContent) {
|
||||||
|
query.join(STATUS.entry.content).fetchJoin();
|
||||||
|
}
|
||||||
|
|
||||||
if (newerThan != null) {
|
if (newerThan != null) {
|
||||||
query.where(status.entryInserted.gt(newerThan));
|
query.where(STATUS.entryInserted.gt(newerThan));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order == ReadingOrder.asc) {
|
if (order == ReadingOrder.asc) {
|
||||||
query.orderBy(status.entryUpdated.asc(), status.id.asc());
|
query.orderBy(STATUS.entryUpdated.asc(), STATUS.id.asc());
|
||||||
} else {
|
} else {
|
||||||
query.orderBy(status.entryUpdated.desc(), status.id.desc());
|
query.orderBy(STATUS.entryUpdated.desc(), STATUS.id.desc());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset > -1) {
|
||||||
|
query.offset(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit > -1) {
|
||||||
|
query.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
query.offset(offset).limit(limit);
|
|
||||||
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
|
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
|
||||||
|
|
||||||
List<FeedEntryStatus> statuses = query.fetch();
|
List<FeedEntryStatus> statuses = query.fetch();
|
||||||
for (FeedEntryStatus status : statuses) {
|
statuses.forEach(s -> s.setMarkable(true));
|
||||||
status = handleStatus(user, status, status.getSubscription(), status.getEntry());
|
if (includeContent) {
|
||||||
fetchTags(user, status);
|
fetchTags(user, statuses);
|
||||||
}
|
|
||||||
return lazyLoadContent(includeContent, statuses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private JPAQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords,
|
return statuses;
|
||||||
Instant newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId,
|
}
|
||||||
Long maxEntryId) {
|
|
||||||
|
|
||||||
JPAQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed()));
|
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
|
||||||
|
List<FeedEntryKeyword> keywords, Instant newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
|
||||||
|
String tag, Long minEntryId, Long maxEntryId) {
|
||||||
|
Map<Long, List<FeedSubscription>> subsByFeedId = subs.stream().collect(Collectors.groupingBy(s -> s.getFeed().getId()));
|
||||||
|
|
||||||
|
JPAQuery<Tuple> query = query().select(ENTRY, STATUS).from(ENTRY);
|
||||||
|
query.leftJoin(ENTRY.statuses, STATUS).on(STATUS.subscription.in(subs));
|
||||||
|
query.where(ENTRY.feed.id.in(subsByFeedId.keySet()));
|
||||||
|
|
||||||
|
if (includeContent || CollectionUtils.isNotEmpty(keywords)) {
|
||||||
|
query.join(ENTRY.content, CONTENT).fetchJoin();
|
||||||
|
}
|
||||||
if (CollectionUtils.isNotEmpty(keywords)) {
|
if (CollectionUtils.isNotEmpty(keywords)) {
|
||||||
query.join(entry.content, content);
|
|
||||||
|
|
||||||
for (FeedEntryKeyword keyword : keywords) {
|
for (FeedEntryKeyword keyword : keywords) {
|
||||||
BooleanBuilder or = new BooleanBuilder();
|
BooleanBuilder or = new BooleanBuilder();
|
||||||
or.or(content.content.containsIgnoreCase(keyword.getKeyword()));
|
or.or(CONTENT.content.containsIgnoreCase(keyword.getKeyword()));
|
||||||
or.or(content.title.containsIgnoreCase(keyword.getKeyword()));
|
or.or(CONTENT.title.containsIgnoreCase(keyword.getKeyword()));
|
||||||
if (keyword.getMode() == Mode.EXCLUDE) {
|
if (keyword.getMode() == Mode.EXCLUDE) {
|
||||||
or.not();
|
or.not();
|
||||||
}
|
}
|
||||||
query.where(or);
|
query.where(or);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
query.leftJoin(entry.statuses, status).on(status.subscription.id.eq(sub.getId()));
|
|
||||||
|
|
||||||
if (unreadOnly && tag == null) {
|
if (unreadOnly && tag == null) {
|
||||||
BooleanBuilder or = new BooleanBuilder();
|
query.where(buildUnreadPredicate());
|
||||||
or.or(status.read.isNull());
|
|
||||||
or.or(status.read.isFalse());
|
|
||||||
query.where(or);
|
|
||||||
|
|
||||||
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
|
||||||
if (unreadThreshold != null) {
|
|
||||||
query.where(entry.updated.goe(unreadThreshold));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
BooleanBuilder and = new BooleanBuilder();
|
BooleanBuilder and = new BooleanBuilder();
|
||||||
and.and(entryTag.user.id.eq(user.getId()));
|
and.and(TAG.user.id.eq(user.getId()));
|
||||||
and.and(entryTag.name.eq(tag));
|
and.and(TAG.name.eq(tag));
|
||||||
query.join(entry.tags, entryTag).on(and);
|
query.join(ENTRY.tags, TAG).on(and);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newerThan != null) {
|
if (newerThan != null) {
|
||||||
query.where(entry.inserted.goe(newerThan));
|
query.where(ENTRY.inserted.goe(newerThan));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minEntryId != null) {
|
if (minEntryId != null) {
|
||||||
query.where(entry.id.gt(minEntryId));
|
query.where(ENTRY.id.gt(minEntryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxEntryId != null) {
|
if (maxEntryId != null) {
|
||||||
query.where(entry.id.lt(maxEntryId));
|
query.where(ENTRY.id.lt(maxEntryId));
|
||||||
}
|
|
||||||
|
|
||||||
if (last != null) {
|
|
||||||
if (order == ReadingOrder.desc) {
|
|
||||||
query.where(entry.updated.gt(last.getEntryUpdated()));
|
|
||||||
} else {
|
|
||||||
query.where(entry.updated.lt(last.getEntryUpdated()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order != null) {
|
if (order != null) {
|
||||||
if (order == ReadingOrder.asc) {
|
if (order == ReadingOrder.asc) {
|
||||||
query.orderBy(entry.updated.asc(), entry.id.asc());
|
query.orderBy(ENTRY.updated.asc(), ENTRY.id.asc());
|
||||||
} else {
|
} else {
|
||||||
query.orderBy(entry.updated.desc(), entry.id.desc());
|
query.orderBy(ENTRY.updated.desc(), ENTRY.id.desc());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,100 +180,58 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
|
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
|
|
||||||
List<FeedEntryKeyword> keywords, Instant newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
|
|
||||||
boolean onlyIds, String tag, Long minEntryId, Long maxEntryId) {
|
|
||||||
int capacity = offset + limit;
|
|
||||||
|
|
||||||
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
|
|
||||||
|
|
||||||
FixedSizeSortedList<FeedEntryStatus> fssl = new FixedSizeSortedList<>(capacity, comparator);
|
|
||||||
for (FeedSubscription sub : subs) {
|
|
||||||
FeedEntryStatus last = (order != null && fssl.isFull()) ? fssl.last() : null;
|
|
||||||
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag, minEntryId,
|
|
||||||
maxEntryId);
|
|
||||||
List<Tuple> tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch();
|
|
||||||
|
|
||||||
|
List<FeedEntryStatus> statuses = new ArrayList<>();
|
||||||
|
List<Tuple> tuples = query.fetch();
|
||||||
for (Tuple tuple : tuples) {
|
for (Tuple tuple : tuples) {
|
||||||
Long id = tuple.get(entry.id);
|
FeedEntry e = tuple.get(ENTRY);
|
||||||
Instant updated = tuple.get(entry.updated);
|
FeedEntryStatus s = tuple.get(STATUS);
|
||||||
Long statusId = tuple.get(status.id);
|
for (FeedSubscription sub : subsByFeedId.get(e.getFeed().getId())) {
|
||||||
|
statuses.add(handleStatus(user, s, sub, e));
|
||||||
FeedEntryContent content = new FeedEntryContent();
|
|
||||||
content.setTitle(tuple.get(entry.content.title));
|
|
||||||
|
|
||||||
FeedEntry entry = new FeedEntry();
|
|
||||||
entry.setId(id);
|
|
||||||
entry.setUpdated(updated);
|
|
||||||
entry.setContent(content);
|
|
||||||
|
|
||||||
FeedEntryStatus status = new FeedEntryStatus();
|
|
||||||
status.setId(statusId);
|
|
||||||
status.setEntryUpdated(updated);
|
|
||||||
status.setEntry(entry);
|
|
||||||
status.setSubscription(sub);
|
|
||||||
|
|
||||||
fssl.add(status);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FeedEntryStatus> placeholders = fssl.asList();
|
if (includeContent) {
|
||||||
int size = placeholders.size();
|
fetchTags(user, statuses);
|
||||||
if (size < offset) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
}
|
||||||
placeholders = placeholders.subList(Math.max(offset, 0), size);
|
|
||||||
|
|
||||||
List<FeedEntryStatus> statuses;
|
|
||||||
if (onlyIds) {
|
|
||||||
statuses = placeholders;
|
|
||||||
} else {
|
|
||||||
statuses = new ArrayList<>();
|
|
||||||
for (FeedEntryStatus placeholder : placeholders) {
|
|
||||||
Long statusId = placeholder.getId();
|
|
||||||
FeedEntry entry = feedEntryDAO.findById(placeholder.getEntry().getId());
|
|
||||||
FeedEntryStatus status = handleStatus(user, statusId == null ? null : findById(statusId), placeholder.getSubscription(),
|
|
||||||
entry);
|
|
||||||
status = fetchTags(user, status);
|
|
||||||
statuses.add(status);
|
|
||||||
}
|
|
||||||
statuses = lazyLoadContent(includeContent, statuses);
|
|
||||||
}
|
|
||||||
return statuses;
|
return statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
|
public UnreadCount getUnreadCount(FeedSubscription sub) {
|
||||||
UnreadCount uc = null;
|
JPAQuery<Tuple> query = query().select(ENTRY.count(), ENTRY.updated.max())
|
||||||
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null, null, null);
|
.from(ENTRY)
|
||||||
List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch();
|
.leftJoin(ENTRY.statuses, STATUS)
|
||||||
for (Tuple tuple : tuples) {
|
.on(STATUS.subscription.eq(sub))
|
||||||
Long count = tuple.get(entry.count());
|
.where(ENTRY.feed.eq(sub.getFeed()))
|
||||||
Instant updated = tuple.get(entry.updated.max());
|
.where(buildUnreadPredicate());
|
||||||
uc = new UnreadCount(subscription.getId(), count == null ? 0 : count, updated);
|
|
||||||
}
|
Tuple tuple = query.fetchOne();
|
||||||
return uc;
|
Long count = tuple.get(ENTRY.count());
|
||||||
|
Instant updated = tuple.get(ENTRY.updated.max());
|
||||||
|
return new UnreadCount(sub.getId(), count == null ? 0 : count, updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FeedEntryStatus> lazyLoadContent(boolean includeContent, List<FeedEntryStatus> results) {
|
private BooleanBuilder buildUnreadPredicate() {
|
||||||
if (includeContent) {
|
BooleanBuilder or = new BooleanBuilder();
|
||||||
for (FeedEntryStatus status : results) {
|
or.or(STATUS.read.isNull());
|
||||||
Models.initialize(status.getSubscription().getFeed());
|
or.or(STATUS.read.isFalse());
|
||||||
Models.initialize(status.getEntry().getContent());
|
|
||||||
|
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||||
|
if (unreadThreshold != null) {
|
||||||
|
return or.and(ENTRY.updated.goe(unreadThreshold));
|
||||||
|
} else {
|
||||||
|
return or;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long deleteOldStatuses(Instant olderThan, int limit) {
|
public long deleteOldStatuses(Instant olderThan, int limit) {
|
||||||
List<Long> ids = query().select(status.id)
|
List<Long> ids = query().select(STATUS.id)
|
||||||
.from(status)
|
.from(STATUS)
|
||||||
.where(status.entryInserted.lt(olderThan), status.starred.isFalse())
|
.where(STATUS.entryInserted.lt(olderThan), STATUS.starred.isFalse())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.fetch();
|
.fetch();
|
||||||
return deleteQuery(status).where(status.id.in(ids)).execute();
|
return deleteQuery(STATUS).where(STATUS.id.in(ids)).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
|
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
|
||||||
|
|
||||||
private final QFeedEntryTag tag = QFeedEntryTag.feedEntryTag;
|
private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedEntryTagDAO(SessionFactory sessionFactory) {
|
public FeedEntryTagDAO(SessionFactory sessionFactory) {
|
||||||
@@ -23,10 +25,18 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> findByUser(User user) {
|
public List<String> findByUser(User user) {
|
||||||
return query().selectDistinct(tag.name).from(tag).where(tag.user.eq(user)).fetch();
|
return query().selectDistinct(TAG.name).from(TAG).where(TAG.user.eq(user)).fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) {
|
public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) {
|
||||||
return query().selectFrom(tag).where(tag.user.eq(user), tag.entry.eq(entry)).fetch();
|
return query().selectFrom(TAG).where(TAG.user.eq(user), TAG.entry.eq(entry)).fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Long, List<FeedEntryTag>> findByEntries(User user, List<FeedEntry> entries) {
|
||||||
|
return query().selectFrom(TAG)
|
||||||
|
.where(TAG.user.eq(user), TAG.entry.in(entries))
|
||||||
|
.fetch()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(t -> t.getEntry().getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ package com.commafeed.backend.dao;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.hibernate.SessionFactory;
|
import org.hibernate.SessionFactory;
|
||||||
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
|
import org.hibernate.event.service.spi.EventListenerRegistry;
|
||||||
|
import org.hibernate.event.spi.EventType;
|
||||||
|
import org.hibernate.event.spi.PostInsertEvent;
|
||||||
|
import org.hibernate.event.spi.PostInsertEventListener;
|
||||||
|
import org.hibernate.persister.entity.EntityPersister;
|
||||||
|
|
||||||
import com.commafeed.backend.model.AbstractModel;
|
import com.commafeed.backend.model.AbstractModel;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
@@ -22,54 +29,79 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
||||||
|
|
||||||
private final QFeedSubscription sub = QFeedSubscription.feedSubscription;
|
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
|
||||||
|
|
||||||
|
private final SessionFactory sessionFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedSubscriptionDAO(SessionFactory sessionFactory) {
|
public FeedSubscriptionDAO(SessionFactory sessionFactory) {
|
||||||
super(sessionFactory);
|
super(sessionFactory);
|
||||||
|
this.sessionFactory = sessionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPostCommitInsert(Consumer<FeedSubscription> consumer) {
|
||||||
|
sessionFactory.unwrap(SessionFactoryImplementor.class)
|
||||||
|
.getServiceRegistry()
|
||||||
|
.getService(EventListenerRegistry.class)
|
||||||
|
.getEventListenerGroup(EventType.POST_COMMIT_INSERT)
|
||||||
|
.appendListener(new PostInsertEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onPostInsert(PostInsertEvent event) {
|
||||||
|
if (event.getEntity() instanceof FeedSubscription s) {
|
||||||
|
consumer.accept(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresPostCommitHandling(EntityPersister persister) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public FeedSubscription findById(User user, Long id) {
|
public FeedSubscription findById(User user, Long id) {
|
||||||
List<FeedSubscription> subs = query().selectFrom(sub)
|
List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION)
|
||||||
.where(sub.user.eq(user), sub.id.eq(id))
|
.where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.id.eq(id))
|
||||||
.leftJoin(sub.feed)
|
.leftJoin(SUBSCRIPTION.feed)
|
||||||
.fetchJoin()
|
.fetchJoin()
|
||||||
.leftJoin(sub.category)
|
.leftJoin(SUBSCRIPTION.category)
|
||||||
.fetchJoin()
|
.fetchJoin()
|
||||||
.fetch();
|
.fetch();
|
||||||
return initRelations(Iterables.getFirst(subs, null));
|
return initRelations(Iterables.getFirst(subs, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedSubscription> findByFeed(Feed feed) {
|
public List<FeedSubscription> findByFeed(Feed feed) {
|
||||||
return query().selectFrom(sub).where(sub.feed.eq(feed)).fetch();
|
return query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.feed.eq(feed)).fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public FeedSubscription findByFeed(User user, Feed feed) {
|
public FeedSubscription findByFeed(User user, Feed feed) {
|
||||||
List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user), sub.feed.eq(feed)).fetch();
|
List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION)
|
||||||
|
.where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed))
|
||||||
|
.fetch();
|
||||||
return initRelations(Iterables.getFirst(subs, null));
|
return initRelations(Iterables.getFirst(subs, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedSubscription> findAll(User user) {
|
public List<FeedSubscription> findAll(User user) {
|
||||||
List<FeedSubscription> subs = query().selectFrom(sub)
|
List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION)
|
||||||
.where(sub.user.eq(user))
|
.where(SUBSCRIPTION.user.eq(user))
|
||||||
.leftJoin(sub.feed)
|
.leftJoin(SUBSCRIPTION.feed)
|
||||||
.fetchJoin()
|
.fetchJoin()
|
||||||
.leftJoin(sub.category)
|
.leftJoin(SUBSCRIPTION.category)
|
||||||
.fetchJoin()
|
.fetchJoin()
|
||||||
.fetch();
|
.fetch();
|
||||||
return initRelations(subs);
|
return initRelations(subs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long count(User user) {
|
public Long count(User user) {
|
||||||
return query().select(sub.count()).from(sub).where(sub.user.eq(user)).fetchOne();
|
return query().select(SUBSCRIPTION.count()).from(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user)).fetchOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
|
public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
|
||||||
JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user));
|
JPQLQuery<FeedSubscription> query = query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user));
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
query.where(sub.category.isNull());
|
query.where(SUBSCRIPTION.category.isNull());
|
||||||
} else {
|
} else {
|
||||||
query.where(sub.category.eq(category));
|
query.where(SUBSCRIPTION.category.eq(category));
|
||||||
}
|
}
|
||||||
return initRelations(query.fetch());
|
return initRelations(query.fetch());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,12 @@ import io.dropwizard.hibernate.AbstractDAO;
|
|||||||
|
|
||||||
public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> {
|
public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> {
|
||||||
|
|
||||||
private final JPAQueryFactory factory;
|
|
||||||
|
|
||||||
protected GenericDAO(SessionFactory sessionFactory) {
|
protected GenericDAO(SessionFactory sessionFactory) {
|
||||||
super(sessionFactory);
|
super(sessionFactory);
|
||||||
this.factory = new JPAQueryFactory(this::currentSession);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected JPAQueryFactory query() {
|
protected JPAQueryFactory query() {
|
||||||
return factory;
|
return new JPAQueryFactory(currentSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected JPAUpdateClause updateQuery(EntityPath<T> entityPath) {
|
protected JPAUpdateClause updateQuery(EntityPath<T> entityPath) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class UserDAO extends GenericDAO<User> {
|
public class UserDAO extends GenericDAO<User> {
|
||||||
|
|
||||||
private final QUser user = QUser.user;
|
private static final QUser USER = QUser.user;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UserDAO(SessionFactory sessionFactory) {
|
public UserDAO(SessionFactory sessionFactory) {
|
||||||
@@ -19,18 +19,18 @@ public class UserDAO extends GenericDAO<User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User findByName(String name) {
|
public User findByName(String name) {
|
||||||
return query().selectFrom(user).where(user.name.equalsIgnoreCase(name)).fetchOne();
|
return query().selectFrom(USER).where(USER.name.equalsIgnoreCase(name)).fetchOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public User findByApiKey(String key) {
|
public User findByApiKey(String key) {
|
||||||
return query().selectFrom(user).where(user.apiKey.equalsIgnoreCase(key)).fetchOne();
|
return query().selectFrom(USER).where(USER.apiKey.equalsIgnoreCase(key)).fetchOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public User findByEmail(String email) {
|
public User findByEmail(String email) {
|
||||||
return query().selectFrom(user).where(user.email.equalsIgnoreCase(email)).fetchOne();
|
return query().selectFrom(USER).where(USER.email.equalsIgnoreCase(email)).fetchOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long count() {
|
public long count() {
|
||||||
return query().select(user.count()).from(user).fetchOne();
|
return query().select(USER.count()).from(USER).fetchOne();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class UserRoleDAO extends GenericDAO<UserRole> {
|
public class UserRoleDAO extends GenericDAO<UserRole> {
|
||||||
|
|
||||||
private final QUserRole role = QUserRole.userRole;
|
private static final QUserRole ROLE = QUserRole.userRole;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UserRoleDAO(SessionFactory sessionFactory) {
|
public UserRoleDAO(SessionFactory sessionFactory) {
|
||||||
@@ -25,11 +25,11 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<UserRole> findAll() {
|
public List<UserRole> findAll() {
|
||||||
return query().selectFrom(role).leftJoin(role.user).fetchJoin().distinct().fetch();
|
return query().selectFrom(ROLE).leftJoin(ROLE.user).fetchJoin().distinct().fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<UserRole> findAll(User user) {
|
public List<UserRole> findAll(User user) {
|
||||||
return query().selectFrom(role).where(role.user.eq(user)).distinct().fetch();
|
return query().selectFrom(ROLE).where(ROLE.user.eq(user)).distinct().fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<Role> findRoles(User user) {
|
public Set<Role> findRoles(User user) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import jakarta.inject.Singleton;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class UserSettingsDAO extends GenericDAO<UserSettings> {
|
public class UserSettingsDAO extends GenericDAO<UserSettings> {
|
||||||
|
|
||||||
private final QUserSettings settings = QUserSettings.userSettings;
|
private static final QUserSettings SETTINGS = QUserSettings.userSettings;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UserSettingsDAO(SessionFactory sessionFactory) {
|
public UserSettingsDAO(SessionFactory sessionFactory) {
|
||||||
@@ -20,6 +20,6 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public UserSettings findByUser(User user) {
|
public UserSettings findByUser(User user) {
|
||||||
return query().selectFrom(settings).where(settings.user.eq(user)).fetchFirst();
|
return query().selectFrom(SETTINGS).where(SETTINGS.user.eq(user)).fetchFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user