mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Compare commits
370 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf02bf221b | ||
|
|
9eb03d7455 | ||
|
|
12c8fdeec2 | ||
|
|
851babfe2a | ||
|
|
859490806b | ||
|
|
2c828b50da | ||
|
|
ede7834cb8 | ||
|
|
3627ee369d | ||
|
|
c4c41d1494 | ||
|
|
c577e77f8f | ||
|
|
9218f19832 | ||
|
|
ecbc2133a4 | ||
|
|
e38ca66c51 | ||
|
|
2395a2670e | ||
|
|
e7748d787f | ||
|
|
012ce71134 | ||
|
|
1b1a3f49c1 | ||
|
|
5b77860189 | ||
|
|
b333e8d90a | ||
|
|
ab6457ef3f | ||
|
|
5c69daec08 | ||
|
|
1bfa3ebb8e | ||
|
|
2694fea211 | ||
|
|
720eddeb66 | ||
|
|
ab334a7bc6 | ||
|
|
214dfe580a | ||
|
|
4ef53eab3a | ||
|
|
2f51547f0d | ||
|
|
da910ac336 | ||
|
|
643954f7c9 | ||
|
|
63061482d0 | ||
|
|
86d4f5a670 | ||
|
|
815093f1c6 | ||
|
|
47d39831d3 | ||
|
|
c18ed829aa | ||
|
|
e757e61b79 | ||
|
|
d612d83874 | ||
|
|
e170dfe60b | ||
|
|
69cd90edd8 | ||
|
|
f506f722c2 | ||
|
|
857736adad | ||
|
|
a92df774bd | ||
|
|
f2c6734c79 | ||
|
|
77b6cf75a5 | ||
|
|
3b56496196 | ||
|
|
aabbf0a5d1 | ||
|
|
9a43fd434f | ||
|
|
21ce9db4b0 | ||
|
|
044694487d | ||
|
|
3af8485326 | ||
|
|
f7adef0648 | ||
|
|
dc16e43154 | ||
|
|
78a5267198 | ||
|
|
04af355e0c | ||
|
|
89405009ec | ||
|
|
6b0aa32da2 | ||
|
|
aaf237d111 | ||
|
|
1fd48a0a40 | ||
|
|
09e0a51b46 | ||
|
|
cc32f8ad16 | ||
|
|
2f6ddf0e70 | ||
|
|
c3973755da | ||
|
|
42537a65b9 | ||
|
|
906c92e54f | ||
|
|
cc69968d78 | ||
|
|
dcde2083ec | ||
|
|
7469784059 | ||
|
|
c13a693456 | ||
|
|
e3c482d664 | ||
|
|
1fd33a5585 | ||
|
|
0742778e6a | ||
|
|
152479c888 | ||
|
|
a297f8c0c8 | ||
|
|
92aeee0572 | ||
|
|
050756517e | ||
|
|
0bb46f291a | ||
|
|
1eecabf105 | ||
|
|
da1bd8d32e | ||
|
|
124983a396 | ||
|
|
43613688da | ||
|
|
43aa69cd18 | ||
|
|
780b7666c5 | ||
|
|
70b4534e14 | ||
|
|
24666fd7fc | ||
|
|
de80aa6bb3 | ||
|
|
6c7e2ea847 | ||
|
|
6ea318acd3 | ||
|
|
2f4ee7cff8 | ||
|
|
9d9d758fa6 | ||
|
|
a071b7c265 | ||
|
|
3a57b68fa3 | ||
|
|
f2f36baf1b | ||
|
|
1aaf9e747a | ||
|
|
92611772a9 | ||
|
|
fb159dc46b | ||
|
|
78ea0873f2 | ||
|
|
faabc01dbc | ||
|
|
5acfe9e92a | ||
|
|
4388a8b6ce | ||
|
|
7414bd15b0 | ||
|
|
52d6021f3c | ||
|
|
f7acc27fcb | ||
|
|
175a293327 | ||
|
|
21dd6519b0 | ||
|
|
72f55c34b7 | ||
|
|
1b1d3c791b | ||
|
|
159c2c01a7 | ||
|
|
272f5b42f9 | ||
|
|
2395d0782e | ||
|
|
da81830e43 | ||
|
|
63a602cf8a | ||
|
|
0244b5c3e3 | ||
|
|
9592e86fa9 | ||
|
|
e6840bb50c | ||
|
|
b6890378a1 | ||
|
|
ba72ed0b93 | ||
|
|
e2fb576858 | ||
|
|
608b099b4d | ||
|
|
c2e0c81f7e | ||
|
|
7071d01a59 | ||
|
|
30cd2b9b53 | ||
|
|
abc498b09c | ||
|
|
31081e1089 | ||
|
|
4a16b8d072 | ||
|
|
9c04095292 | ||
|
|
643f98d59e | ||
|
|
f4da19183e | ||
|
|
de40f253b5 | ||
|
|
f6543e407a | ||
|
|
4d462a8e9e | ||
|
|
018ee1f3e6 | ||
|
|
752268fed1 | ||
|
|
8fe9a6cc3a | ||
|
|
b17a17ba10 | ||
|
|
b3545b60ea | ||
|
|
e6da3f693d | ||
|
|
4ab09da434 | ||
|
|
5e8daf29bf | ||
|
|
024a1067bb | ||
|
|
c427da72b9 | ||
|
|
346fb6b1ea | ||
|
|
1b658c76a3 | ||
|
|
1593ed62ba | ||
|
|
085eddd4b0 | ||
|
|
0db77ad2c0 | ||
|
|
6f8bcb6c6a | ||
|
|
4196dee896 | ||
|
|
6d49e0f0df | ||
|
|
d99f572989 | ||
|
|
fa197c33f1 | ||
|
|
1ce39a419e | ||
|
|
f0e3ac8fcb | ||
|
|
30947cea05 | ||
|
|
9134f36d3b | ||
|
|
dc526316a0 | ||
|
|
6593174668 | ||
|
|
0891c41abc | ||
|
|
6ecb6254aa | ||
|
|
84bd9eeeff | ||
|
|
2549c4d47b | ||
|
|
8750aa3dd6 | ||
|
|
262094a736 | ||
|
|
035201f917 | ||
|
|
ae9cbc5214 | ||
|
|
78d5bf129a | ||
|
|
1f02ddd163 | ||
|
|
eff1e8cc7b | ||
|
|
dc8475b59a | ||
|
|
921968662d | ||
|
|
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 |
@@ -1,6 +1 @@
|
|||||||
# ignore everything
|
commafeed-client
|
||||||
*
|
|
||||||
|
|
||||||
# allow only what we need
|
|
||||||
!commafeed-server/target/commafeed.jar
|
|
||||||
!commafeed-server/config.yml.example
|
|
||||||
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
|
|
||||||
89
.github/workflows/build.yml
vendored
89
.github/workflows/build.yml
vendored
@@ -1,89 +0,0 @@
|
|||||||
name: Java CI
|
|
||||||
|
|
||||||
on: [ push ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
java: [ "17", "21" ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Setup
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Set up Java
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: ${{ matrix.java }}
|
|
||||||
distribution: "temurin"
|
|
||||||
cache: "maven"
|
|
||||||
|
|
||||||
# Build
|
|
||||||
- name: Build with Maven
|
|
||||||
run: mvn --batch-mode --update-snapshots verify
|
|
||||||
|
|
||||||
- name: Upload JAR
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ matrix.java == '17' }}
|
|
||||||
with:
|
|
||||||
name: commafeed.jar
|
|
||||||
path: commafeed-server/target/commafeed.jar
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
- name: Login to Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }}
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Docker build and push tag
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
|
||||||
tags: |
|
|
||||||
athou/commafeed:latest
|
|
||||||
athou/commafeed:${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Docker build and push master
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
|
||||||
tags: athou/commafeed:master
|
|
||||||
|
|
||||||
# Create GitHub release after Docker image has been published
|
|
||||||
- name: Extract Changelog Entry
|
|
||||||
uses: mindsers/changelog-reader-action@v2
|
|
||||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
|
||||||
id: changelog_reader
|
|
||||||
with:
|
|
||||||
version: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Create GitHub release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
|
||||||
with:
|
|
||||||
name: CommaFeed ${{ github.ref_name }}
|
|
||||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
files: |
|
|
||||||
commafeed-server/target/commafeed.jar
|
|
||||||
commafeed-server/config.yml.example
|
|
||||||
183
.github/workflows/ci.yml
vendored
Normal file
183
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on: [ push ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
JAVA_VERSION: 21
|
||||||
|
DOCKER_BUILD_SUMMARY: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checkout
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Set up GraalVM
|
||||||
|
uses: graalvm/setup-graalvm@v1
|
||||||
|
with:
|
||||||
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
|
distribution: "graalvm"
|
||||||
|
cache: "maven"
|
||||||
|
|
||||||
|
# Build & Test
|
||||||
|
- name: Build with Maven
|
||||||
|
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }}
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
- name: Upload cross-platform app
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: commafeed-${{ matrix.database }}-jvm
|
||||||
|
path: commafeed-server/target/commafeed-*.zip
|
||||||
|
|
||||||
|
- name: Upload native executable
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
path: commafeed-server/target/commafeed-*-runner
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }}
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
## tags
|
||||||
|
- name: Docker build and push tag - native
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: |
|
||||||
|
athou/commafeed:latest-${{ matrix.database }}
|
||||||
|
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||||
|
|
||||||
|
- name: Docker build and push tag - jvm
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
tags: |
|
||||||
|
athou/commafeed:latest-${{ matrix.database }}-jvm
|
||||||
|
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
|
||||||
|
|
||||||
|
## master
|
||||||
|
- name: Docker build and push master - native
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
if: ${{ github.ref_name == 'master' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: athou/commafeed:master-${{ matrix.database }}
|
||||||
|
|
||||||
|
- name: Docker build and push master - jvm
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
if: ${{ github.ref_name == 'master' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
tags: athou/commafeed:master-${{ matrix.database }}-jvm
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checkout
|
||||||
|
- name: Configure git to checkout as-is
|
||||||
|
run: git config --global core.autocrlf false
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
- name: Set up GraalVM
|
||||||
|
uses: graalvm/setup-graalvm@v1
|
||||||
|
with:
|
||||||
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
|
distribution: "graalvm"
|
||||||
|
cache: "maven"
|
||||||
|
|
||||||
|
# Build & Test
|
||||||
|
- name: Build with Maven
|
||||||
|
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.database != 'h2' }}
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
- name: Upload native executable
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
path: commafeed-server/target/commafeed-*-runner.exe
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build-linux
|
||||||
|
- build-windows
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: commafeed-*
|
||||||
|
path: ./artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Extract Changelog Entry
|
||||||
|
uses: mindsers/changelog-reader-action@v2
|
||||||
|
id: changelog_reader
|
||||||
|
with:
|
||||||
|
version: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Create GitHub release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
name: CommaFeed ${{ github.ref_name }}
|
||||||
|
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||||
|
artifacts: ./artifacts/*
|
||||||
|
|
||||||
|
- name: Update Docker Hub Description
|
||||||
|
uses: peter-evans/dockerhub-description@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
repository: athou/commafeed
|
||||||
|
short-description: ${{ github.event.repository.description }}
|
||||||
|
readme-filepath: commafeed-server/src/main/docker/README.md
|
||||||
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.9/apache-maven-3.9.9-bin.zip
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,5 +1,59 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [5.0.0]
|
||||||
|
|
||||||
|
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
|
||||||
|
the [announcement](https://github.com/Athou/commafeed/discussions/1517).
|
||||||
|
The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around
|
||||||
|
0.3s) and very low memory footprint (< 50M).
|
||||||
|
|
||||||
|
- CommaFeed now has a different package for each supported database.
|
||||||
|
- If you are deploying CommaFeed with a precompiled package, please
|
||||||
|
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#download-a-precompiled-package).
|
||||||
|
- If you are building CommaFeed from sources, please
|
||||||
|
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#build-from-sources).
|
||||||
|
- If you are using the Docker image, please read the instructions on
|
||||||
|
the [Docker Hub page](https://hub.docker.com/r/athou/commafeed).
|
||||||
|
- Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone).
|
||||||
|
Please
|
||||||
|
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration).
|
||||||
|
Note that a lot of configuration elements have been removed or renamed and are now nested/grouped by feature.
|
||||||
|
- Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB.
|
||||||
|
- Use a different icon for filtering unread entries and marking an entry as read (#1506)
|
||||||
|
- Added various HTML attributes to ease custom JS/CSS customization (#1507)
|
||||||
|
- The Redis cache has been removed. There have been multiple enhancements to the feed refresh engine and it is no longer
|
||||||
|
needed, even for instances with a large number of feeds.
|
||||||
|
- The H2 migration tool that automatically upgrades H2 databases from format 2 to 3 has been removed. If you're using
|
||||||
|
the H2 embedded database, please upgrade to at least version 4.3.0 before upgrading to CommaFeed 5.0.0.
|
||||||
|
|
||||||
|
## [4.6.0]
|
||||||
|
|
||||||
|
- switched from Temurin to OpenJ9 as the JVM used in the Docker image, resulting in memory usage reduction by up to 50%
|
||||||
|
- fix an issue that could cause old entries to reappear if they were updated by their author (#1486)
|
||||||
|
- show all entries regardless of their read status when searching with keywords, even if the ui is configured to show
|
||||||
|
unread entries only
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@@ -1,12 +0,0 @@
|
|||||||
FROM eclipse-temurin:17-jre
|
|
||||||
|
|
||||||
EXPOSE 8082
|
|
||||||
|
|
||||||
RUN mkdir -p /commafeed/data
|
|
||||||
VOLUME /commafeed/data
|
|
||||||
|
|
||||||
COPY commafeed-server/config.yml.example config.yml
|
|
||||||
COPY commafeed-server/target/commafeed.jar .
|
|
||||||
|
|
||||||
ENV JAVA_TOOL_OPTIONS -Djava.net.preferIPv4Stack=true -Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
|
|
||||||
CMD ["java", "-jar", "commafeed.jar", "server", "config.yml"]
|
|
||||||
110
README.md
110
README.md
@@ -1,6 +1,6 @@
|
|||||||
# CommaFeed
|
# CommaFeed
|
||||||
|
|
||||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
|
Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeScript.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -8,14 +8,22 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
|
|||||||
|
|
||||||
- 4 different layouts
|
- 4 different layouts
|
||||||
- Light/Dark theme
|
- Light/Dark theme
|
||||||
- Fully responsive
|
- Fully responsive, works great on both mobile and desktop
|
||||||
- Keyboard shortcuts for almost everything
|
- Keyboard shortcuts for almost everything
|
||||||
- Support for right-to-left feeds
|
- Support for right-to-left feeds
|
||||||
- Translated in 25+ languages
|
- Translated in 25+ languages
|
||||||
- Supports thousands of users and millions of feeds
|
- Supports thousands of users and millions of feeds
|
||||||
- OPML import/export
|
- OPML import/export
|
||||||
- REST API and a Fever-compatible API for native mobile apps
|
- REST API
|
||||||
|
- Fever-compatible API for native mobile apps
|
||||||
|
- Can automatically mark articles as read based on user-defined rules
|
||||||
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
|
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
|
||||||
|
- Compiles to native code for blazing fast startup and low memory usage
|
||||||
|
- Supports 4 databases
|
||||||
|
- H2 (embedded database)
|
||||||
|
- PostgreSQL
|
||||||
|
- MySQL
|
||||||
|
- MariaDB
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
@@ -33,32 +41,79 @@ PikaPods shares 20% of the revenue back to CommaFeed.
|
|||||||
|
|
||||||
[](https://www.pikapods.com/pods?run=commafeed)
|
[](https://www.pikapods.com/pods?run=commafeed)
|
||||||
|
|
||||||
### Download precompiled package
|
### Download a precompiled package
|
||||||
|
|
||||||
mkdir commafeed && cd commafeed
|
Go to the [release page](https://github.com/Athou/commafeed/releases) and download the latest version for your operating
|
||||||
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
|
system and database of choice.
|
||||||
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
|
|
||||||
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
|
|
||||||
|
|
||||||
The server will listen on http://localhost:8082. The default
|
There are two types of packages:
|
||||||
user is `admin` and the default password is `admin`.
|
|
||||||
|
- The `linux-x86_64` and `windows-x86_64` packages are compiled natively and contain an executable that can be run
|
||||||
|
directly.
|
||||||
|
- The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all
|
||||||
|
platforms and is started with `java -jar quarkus-run.jar`.
|
||||||
|
|
||||||
|
If available for your operating system, the native package is recommended because it has a faster startup time and lower
|
||||||
|
memory usage.
|
||||||
|
|
||||||
### Build from sources
|
### Build from sources
|
||||||
|
|
||||||
git clone https://github.com/Athou/commafeed.git
|
./mvnw clean package [-P<database>] [-Pnative] [-DskipTests]
|
||||||
cd commafeed
|
|
||||||
./mvnw clean package
|
|
||||||
cp commafeed-server/config.yml.example config.yml
|
|
||||||
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
|
||||||
|
|
||||||
The server will listen on http://localhost:8082. The default
|
- `<database>` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. The default is `h2`.
|
||||||
user is `admin` and the default password is `admin`.
|
- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment
|
||||||
|
variable pointing to a GraalVM installation).
|
||||||
|
- `-DskipTests` to speed up the build process by skipping tests.
|
||||||
|
|
||||||
### Memory management
|
When the build is complete:
|
||||||
|
|
||||||
|
- a zip containing all jars required to run the application is located at
|
||||||
|
`commafeed-server/target/commafeed-<version>-<database>-jvm.zip`. Extract it and run the application with
|
||||||
|
`java -jar quarkus-run.jar`
|
||||||
|
- if you used the native profile, the executable is located at
|
||||||
|
`commafeed-server/target/commafeed-<version>-<database>-<platform>-<arch>-runner[.exe]`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in
|
||||||
|
the `data` directory of the current directory.
|
||||||
|
|
||||||
|
To use a different database, you will need to configure the following properties:
|
||||||
|
|
||||||
|
- `quarkus.datasource.jdbc-url`
|
||||||
|
- e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE`
|
||||||
|
- e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed`
|
||||||
|
- e.g. for MySQL:
|
||||||
|
`jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||||
|
- e.g. for MariaDB:
|
||||||
|
`jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||||
|
- `quarkus.datasource.username`
|
||||||
|
- `quarkus.datasource.password`
|
||||||
|
|
||||||
|
There are multiple ways to configure CommaFeed:
|
||||||
|
|
||||||
|
- a [properties](https://en.wikipedia.org/wiki/.properties) file in `config/application.properties` (keys in kebab-case)
|
||||||
|
- Command line arguments prefixed with `-D` (keys in kebab-case)
|
||||||
|
- Environment variables (keys in UPPER_CASE)
|
||||||
|
- an .env file in the working directory (keys in UPPER_CASE)
|
||||||
|
|
||||||
|
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
|
||||||
|
|
||||||
|
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
|
||||||
|
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
|
||||||
|
`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters).
|
||||||
|
|
||||||
|
All [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java)
|
||||||
|
are optional and have sensible default values.
|
||||||
|
|
||||||
|
When started, the server will listen on http://localhost:8082.
|
||||||
|
The default user is `admin` and the default password is `admin`.
|
||||||
|
|
||||||
|
### Memory management (`jvm` package only)
|
||||||
|
|
||||||
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
|
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
|
||||||
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
|
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
|
||||||
However, this can be problematic on systems with limited memory.
|
This can be problematic on systems with limited memory.
|
||||||
|
|
||||||
#### Hard limit
|
#### Hard limit
|
||||||
|
|
||||||
@@ -67,16 +122,25 @@ For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
|
|||||||
|
|
||||||
#### Dynamic sizing
|
#### Dynamic sizing
|
||||||
|
|
||||||
The JVM can be configured to release unused memory to the operating system with the following parameters:
|
In addition to the previous setting, the JVM can be configured to release unused memory to the operating system with the
|
||||||
|
following parameters:
|
||||||
|
|
||||||
-Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
|
-Xms20m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
|
||||||
|
|
||||||
This is how the Docker image is configured.
|
|
||||||
See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
|
See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
|
||||||
and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for
|
and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for
|
||||||
more
|
more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
|
#### OpenJ9
|
||||||
|
|
||||||
|
The [OpenJ9](https://eclipse.dev/openj9/) JVM is a more memory-efficient alternative to the HotSpot JVM, at the cost of
|
||||||
|
slightly slower throughput.
|
||||||
|
|
||||||
|
IBM provides precompiled binaries for OpenJ9
|
||||||
|
named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/).
|
||||||
|
This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/blob/master/Dockerfile).
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
Files for internationalization are
|
Files for internationalization are
|
||||||
@@ -99,7 +163,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
|
|||||||
|
|
||||||
- Open `commafeed-server` in your preferred Java IDE.
|
- Open `commafeed-server` in your preferred Java IDE.
|
||||||
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
||||||
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
|
- run `./mvnw quarkus:dev`
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
|
|||||||
@@ -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,52 +0,0 @@
|
|||||||
{
|
|
||||||
"locales": [
|
|
||||||
"ar",
|
|
||||||
"ca",
|
|
||||||
"cs",
|
|
||||||
"cy",
|
|
||||||
"da",
|
|
||||||
"de",
|
|
||||||
"en",
|
|
||||||
"es",
|
|
||||||
"fa",
|
|
||||||
"fi",
|
|
||||||
"fr",
|
|
||||||
"gl",
|
|
||||||
"hu",
|
|
||||||
"id",
|
|
||||||
"it",
|
|
||||||
"ja",
|
|
||||||
"ko",
|
|
||||||
"ms",
|
|
||||||
"nb",
|
|
||||||
"nl",
|
|
||||||
"nn",
|
|
||||||
"pl",
|
|
||||||
"pt",
|
|
||||||
"ru",
|
|
||||||
"sk",
|
|
||||||
"sv",
|
|
||||||
"tr",
|
|
||||||
"zh"
|
|
||||||
],
|
|
||||||
"catalogs": [
|
|
||||||
{
|
|
||||||
"path": "src/locales/{locale}/messages",
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"src/locales/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"format": "po",
|
|
||||||
"formatOptions": {
|
|
||||||
"origins": true,
|
|
||||||
"lineNumbers": false
|
|
||||||
},
|
|
||||||
"sourceLocale": "en",
|
|
||||||
"fallbackLocales": {
|
|
||||||
"default": "en"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
52
commafeed-client/lingui.config.ts
Normal file
52
commafeed-client/lingui.config.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { LinguiConfig } from "@lingui/conf"
|
||||||
|
|
||||||
|
const config: LinguiConfig = {
|
||||||
|
locales: [
|
||||||
|
"ar",
|
||||||
|
"ca",
|
||||||
|
"cs",
|
||||||
|
"cy",
|
||||||
|
"da",
|
||||||
|
"de",
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"fa",
|
||||||
|
"fi",
|
||||||
|
"fr",
|
||||||
|
"gl",
|
||||||
|
"hu",
|
||||||
|
"id",
|
||||||
|
"it",
|
||||||
|
"ja",
|
||||||
|
"ko",
|
||||||
|
"ms",
|
||||||
|
"nb",
|
||||||
|
"nl",
|
||||||
|
"nn",
|
||||||
|
"pl",
|
||||||
|
"pt",
|
||||||
|
"ru",
|
||||||
|
"sk",
|
||||||
|
"sv",
|
||||||
|
"tr",
|
||||||
|
"zh",
|
||||||
|
],
|
||||||
|
catalogs: [
|
||||||
|
{
|
||||||
|
path: "src/locales/{locale}/messages",
|
||||||
|
include: ["src"],
|
||||||
|
exclude: ["src/locales/**"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
format: "po",
|
||||||
|
formatOptions: {
|
||||||
|
origins: true,
|
||||||
|
lineNumbers: false,
|
||||||
|
},
|
||||||
|
sourceLocale: "en",
|
||||||
|
fallbackLocales: {
|
||||||
|
default: "en",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
18890
commafeed-client/package-lock.json
generated
18890
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,83 +1,79 @@
|
|||||||
{
|
{
|
||||||
"name": "commafeed-client",
|
"name": "commafeed-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"dev:typescript": "tsc --watch",
|
"dev:typescript": "tsc --watch",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"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",
|
||||||
"i18n:extract": "lingui extract --clean"
|
"lint:fix": "biome check --write ./src",
|
||||||
},
|
"i18n:extract": "lingui extract --clean"
|
||||||
"dependencies": {
|
},
|
||||||
"@emotion/react": "^11.11.4",
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^5.0.27",
|
"@emotion/react": "^11.13.0",
|
||||||
"@lingui/core": "^4.10.0",
|
"@fontsource/open-sans": "^5.0.29",
|
||||||
"@lingui/macro": "^4.10.0",
|
"@lingui/core": "^4.11.3",
|
||||||
"@lingui/react": "^4.10.0",
|
"@lingui/macro": "^4.11.3",
|
||||||
"@mantine/core": "^7.8.0",
|
"@lingui/react": "^4.11.3",
|
||||||
"@mantine/form": "^7.8.0",
|
"@mantine/core": "^7.12.1",
|
||||||
"@mantine/hooks": "^7.8.0",
|
"@mantine/form": "^7.12.1",
|
||||||
"@mantine/modals": "^7.8.0",
|
"@mantine/hooks": "^7.12.1",
|
||||||
"@mantine/notifications": "^7.8.0",
|
"@mantine/modals": "^7.12.1",
|
||||||
"@mantine/spotlight": "^7.8.0",
|
"@mantine/notifications": "^7.12.1",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@mantine/spotlight": "^7.12.1",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"axios": "^1.6.8",
|
"@reduxjs/toolkit": "^2.2.7",
|
||||||
"dayjs": "^1.11.10",
|
"axios": "^1.7.4",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"dayjs": "^1.11.12",
|
||||||
"interweave": "^13.1.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"monaco-editor": "^0.47.0",
|
"interweave": "^13.1.0",
|
||||||
"mousetrap": "^1.6.5",
|
"monaco-editor": "^0.50.0",
|
||||||
"react": "^18.2.0",
|
"mousetrap": "^1.6.5",
|
||||||
"react-async-hook": "^4.0.0",
|
"react": "^18.3.1",
|
||||||
"react-contexify": "^6.0.0",
|
"react-async-hook": "^4.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-contexify": "^6.0.0",
|
||||||
"react-draggable": "^4.4.6",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-ga4": "^2.1.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.0.1",
|
"react-draggable": "^4.4.6",
|
||||||
"react-infinite-scroller": "^1.2.6",
|
"react-ga4": "^2.1.0",
|
||||||
"react-redux": "^9.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-icons": "^5.2.1",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-infinite-scroller": "^1.2.6",
|
||||||
"redoc": "^2.1.3",
|
"react-redux": "^9.1.2",
|
||||||
"throttle-debounce": "^5.0.0",
|
"react-router-dom": "^6.26.1",
|
||||||
"tinycon": "^0.6.8",
|
"react-swipeable": "^7.0.1",
|
||||||
"tss-react": "^4.9.6",
|
"redoc": "^2.1.5",
|
||||||
"use-local-storage": "^3.0.0",
|
"throttle-debounce": "^5.0.2",
|
||||||
"websocket-heartbeat-js": "^1.1.3"
|
"tinycon": "^0.6.8",
|
||||||
},
|
"tss-react": "^4.9.12",
|
||||||
"devDependencies": {
|
"use-local-storage": "^3.0.0",
|
||||||
"@lingui/cli": "^4.10.0",
|
"vite-plugin-biome": "^1.0.12",
|
||||||
"@lingui/vite-plugin": "^4.10.0",
|
"websocket-heartbeat-js": "^1.1.3"
|
||||||
"@types/mousetrap": "^1.6.15",
|
},
|
||||||
"@types/react": "^18.2.78",
|
"devDependencies": {
|
||||||
"@types/react-dom": "^18.2.25",
|
"@biomejs/biome": "^1.8.3",
|
||||||
"@types/react-infinite-scroller": "^1.2.5",
|
"@lingui/cli": "^4.11.3",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@lingui/vite-plugin": "^4.11.3",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/mousetrap": "^1.6.15",
|
||||||
"@types/tinycon": "^0.6.5",
|
"@types/react": "^18.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@types/react-helmet": "^6.1.11",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"@types/react-infinite-scroller": "^1.2.5",
|
||||||
"eslint": "^8.57.0",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"eslint-config-love": "^47.0.0",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"@types/tinycon": "^0.6.5",
|
||||||
"eslint-config-standard": "^17.1.0",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"typescript": "^5.5.4",
|
||||||
"prettier": "^3.2.5",
|
"vite": "^5.4.1",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"vite-tsconfig-paths": "^5.0.1",
|
||||||
"typescript": "^5.4.5",
|
"vitest": "^2.0.5",
|
||||||
"vite": "^5.2.8",
|
"vitest-mock-extended": "^2.0.0"
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
}
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
|
||||||
"vitest": "^1.5.0",
|
|
||||||
"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>5.0.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.16.0</node.version>
|
||||||
|
<!-- renovate: datasource=npm depName=npm -->
|
||||||
|
<npm.version>10.8.2</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>
|
||||||
@@ -72,7 +80,7 @@
|
|||||||
<goal>copy-resources</goal>
|
<goal>copy-resources</goal>
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<outputDirectory>${project.build.directory}/classes/assets</outputDirectory>
|
<outputDirectory>${project.build.directory}/classes/META-INF/resources</outputDirectory>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
<directory>dist</directory>
|
<directory>dist</directory>
|
||||||
|
|||||||
@@ -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 })
|
||||||
@@ -81,7 +81,17 @@ export const client = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
|
login: async (req: LoginRequest) => {
|
||||||
|
const formData = new URLSearchParams()
|
||||||
|
formData.append("j_username", req.name)
|
||||||
|
formData.append("j_password", req.password)
|
||||||
|
return await axiosInstance.post("j_security_check", formData, {
|
||||||
|
baseURL: ".",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
|
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
|
||||||
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
|
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
|
||||||
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
|
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
|
||||||
|
|||||||
@@ -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,11 +1,11 @@
|
|||||||
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 { any, mockReset } from "vitest-mock-extended"
|
||||||
|
|
||||||
const mockClient = await vi.hoisted(async () => {
|
const mockClient = await vi.hoisted(async () => {
|
||||||
const mockModule = await import("vitest-mock-extended")
|
const mockModule = await import("vitest-mock-extended")
|
||||||
@@ -19,7 +19,7 @@ describe("entries", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("loads entries", async () => {
|
it("loads entries", async () => {
|
||||||
mockClient.feed.getEntries.mockResolvedValue({
|
mockClient.feed.getEntries.calledWith(any()).mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
entries: [{ id: "3" } as Entry],
|
entries: [{ id: "3" } as Entry],
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
@@ -53,7 +53,7 @@ describe("entries", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("loads more entries", async () => {
|
it("loads more entries", async () => {
|
||||||
mockClient.category.getEntries.mockResolvedValue({
|
mockClient.category.getEntries.calledWith(any()).mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
entries: [{ id: "4" } as Entry],
|
entries: [{ id: "4" } as Entry],
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
|
|||||||
@@ -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)
|
e.expanded = action.payload.expanded
|
||||||
.forEach(e => {
|
}
|
||||||
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)
|
e.read = action.meta.arg.read
|
||||||
.forEach(e => {
|
}
|
||||||
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))
|
e.read = action.meta.arg.read
|
||||||
.forEach(e => {
|
}
|
||||||
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))
|
e.read = true
|
||||||
.forEach(e => {
|
}
|
||||||
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)
|
e.starred = action.meta.arg.starred
|
||||||
.forEach(e => {
|
}
|
||||||
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)
|
e.tags = action.meta.arg.tags
|
||||||
.forEach(e => {
|
}
|
||||||
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"
|
||||||
@@ -40,7 +40,7 @@ export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_,
|
|||||||
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
|
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
|
||||||
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
||||||
order: state.user.settings?.readingOrder,
|
order: state.user.settings?.readingOrder,
|
||||||
readType: state.user.settings?.readingMode,
|
readType: state.entries.search ? "all" : state.user.settings?.readingMode,
|
||||||
offset,
|
offset,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
tag: source.type === "tag" ? source.id : undefined,
|
tag: source.type === "tag" ? source.id : undefined,
|
||||||
|
|||||||
@@ -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)
|
f.unread += action.payload.amount
|
||||||
.forEach(f => {
|
}
|
||||||
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)
|
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
|
||||||
.forEach(f => {
|
}
|
||||||
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,13 +1,15 @@
|
|||||||
|
import type { MessageDescriptor } from "@lingui/core"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
label: ReactNode
|
label?: string | MessageDescriptor
|
||||||
onClick?: MouseEventHandler
|
onClick?: MouseEventHandler
|
||||||
variant?: ActionIconVariant & ButtonVariant
|
variant?: ActionIconVariant & ButtonVariant
|
||||||
hideLabelOnDesktop?: boolean
|
hideLabelOnDesktop?: boolean
|
||||||
@@ -20,17 +22,35 @@ interface ActionButtonProps {
|
|||||||
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
||||||
const { mobile } = useActionButton()
|
const { mobile } = useActionButton()
|
||||||
const theme = useMantineTheme()
|
const theme = useMantineTheme()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
|
const label = typeof props.label === "string" ? props.label : props.label && _(props.label)
|
||||||
const variant = props.variant ?? "subtle"
|
const variant = props.variant ?? "subtle"
|
||||||
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
||||||
return iconOnly ? (
|
return iconOnly ? (
|
||||||
<Tooltip label={props.label} openDelay={Constants.tooltip.delay}>
|
<Tooltip label={label} openDelay={Constants.tooltip.delay}>
|
||||||
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
|
<ActionIcon
|
||||||
|
ref={ref}
|
||||||
|
color={theme.primaryColor}
|
||||||
|
variant={variant}
|
||||||
|
className={props.className}
|
||||||
|
onClick={props.onClick}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
{props.icon}
|
{props.icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Button ref={ref} variant={variant} size="xs" className={props.className} leftSection={props.icon} onClick={props.onClick}>
|
<Button
|
||||||
{props.label}
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size="xs"
|
||||||
|
className={props.className}
|
||||||
|
leftSection={props.icon}
|
||||||
|
onClick={props.onClick}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -148,9 +150,7 @@ export function KeyboardShortcutsHelp() {
|
|||||||
<Trans>Navigate to a subscription by entering its name</Trans>
|
<Trans>Navigate to a subscription by entering its name</Trans>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Kbd>
|
<Kbd>{isMacOS ? <Trans>Cmd</Trans> : <Trans>Ctrl</Trans>}</Kbd>
|
||||||
<Trans>Ctrl</Trans>
|
|
||||||
</Kbd>
|
|
||||||
<span> + </span>
|
<span> + </span>
|
||||||
<Kbd>K</Kbd>
|
<Kbd>K</Kbd>
|
||||||
<span>, </span>
|
<span>, </span>
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -305,11 +305,12 @@ export function FeedEntries() {
|
|||||||
loader={<Box key={0}>{loading && <Loader />}</Box>}
|
loader={<Box key={0}>{loading && <Loader />}</Box>}
|
||||||
>
|
>
|
||||||
{entries.map(entry => (
|
{entries.map(entry => (
|
||||||
<div
|
<article
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
ref={el => {
|
ref={el => {
|
||||||
if (el) el.id = Constants.dom.entryId(entry)
|
if (el) el.id = Constants.dom.entryId(entry)
|
||||||
}}
|
}}
|
||||||
|
data-id={entry.id}
|
||||||
>
|
>
|
||||||
<FeedEntry
|
<FeedEntry
|
||||||
entry={entry}
|
entry={entry}
|
||||||
@@ -322,7 +323,7 @@ export function FeedEntries() {
|
|||||||
onBodyClick={() => bodyClicked(entry)}
|
onBodyClick={() => bodyClicked(entry)}
|
||||||
onSwipedLeft={async () => await swipedLeft(entry)}
|
onSwipedLeft={async () => await swipedLeft(entry)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</article>
|
||||||
))}
|
))}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,12 +4,12 @@ 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"
|
||||||
import { Item, Menu, Separator } from "react-contexify"
|
import { Item, Menu, Separator } from "react-contexify"
|
||||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
|
|
||||||
interface FeedEntryContextMenuProps {
|
interface FeedEntryContextMenuProps {
|
||||||
@@ -70,7 +70,7 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
|||||||
{props.entry.markable && (
|
{props.entry.markable && (
|
||||||
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
||||||
<Group>
|
<Group>
|
||||||
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
|
{props.entry.read ? <TbMail size={iconSize} /> : <TbMailOpened size={iconSize} />}
|
||||||
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||||
</Group>
|
</Group>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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"
|
||||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||||
import { ShareButtons } from "./ShareButtons"
|
import { ShareButtons } from "./ShareButtons"
|
||||||
|
|
||||||
interface FeedEntryFooterProps {
|
interface FeedEntryFooterProps {
|
||||||
@@ -18,6 +19,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
const mobile = useMobile()
|
const mobile = useMobile()
|
||||||
const { spacing } = useActionButton()
|
const { spacing } = useActionButton()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const readStatusButtonClicked = async () =>
|
const readStatusButtonClicked = async () =>
|
||||||
await dispatch(
|
await dispatch(
|
||||||
@@ -39,14 +41,14 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
<Group gap={spacing}>
|
<Group gap={spacing}>
|
||||||
{props.entry.markable && (
|
{props.entry.markable && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
|
icon={props.entry.read ? <TbMail size={18} /> : <TbMailOpened size={18} />}
|
||||||
label={props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
label={props.entry.read ? msg`Keep unread` : msg`Mark as read`}
|
||||||
onClick={readStatusButtonClicked}
|
onClick={readStatusButtonClicked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
|
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
|
||||||
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
label={props.entry.starred ? msg`Unstar` : msg`Star`}
|
||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
await dispatch(
|
await dispatch(
|
||||||
starEntry({
|
starEntry({
|
||||||
@@ -59,7 +61,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
|
|
||||||
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
|
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
|
<ActionButton icon={<TbShare size={18} />} label={msg`Share`} />
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<ShareButtons url={props.entry.url} description={props.entry.title} />
|
<ShareButtons url={props.entry.url} description={props.entry.title} />
|
||||||
@@ -70,12 +72,12 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
<Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
|
<Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
|
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
|
||||||
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
|
<ActionButton icon={<TbTag size={18} />} label={msg`Tags`} />
|
||||||
</Indicator>
|
</Indicator>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<TagsInput
|
<TagsInput
|
||||||
placeholder={t`Tags`}
|
placeholder={_(msg`Tags`)}
|
||||||
data={tags}
|
data={tags}
|
||||||
value={props.entry.tags}
|
value={props.entry.tags}
|
||||||
onChange={onTagsChange}
|
onChange={onTagsChange}
|
||||||
@@ -88,13 +90,13 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<a href={props.entry.url} target="_blank" rel="noreferrer">
|
<a href={props.entry.url} target="_blank" rel="noreferrer">
|
||||||
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
|
<ActionButton icon={<TbExternalLink size={18} />} label={msg`Open link`} />
|
||||||
</a>
|
</a>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbArrowBarToDown size={18} />}
|
icon={<TbArrowBarToDown size={18} />}
|
||||||
label={<Trans>Mark as read up to here</Trans>}
|
label={msg`Mark as read up to here`}
|
||||||
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
|
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -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,12 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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"
|
||||||
@@ -13,6 +14,7 @@ import { CategorySelect } from "./CategorySelect"
|
|||||||
|
|
||||||
export function AddCategory() {
|
export function AddCategory() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const form = useForm<AddCategoryRequest>()
|
const form = useForm<AddCategoryRequest>()
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ export function AddCategory() {
|
|||||||
|
|
||||||
<form onSubmit={form.onSubmit(addCategory.execute)}>
|
<form onSubmit={form.onSubmit(addCategory.execute)}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
|
<TextInput label={<Trans>Category</Trans>} placeholder={_(msg`Category`)} {...form.getInputProps("name")} required />
|
||||||
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
|
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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> & {
|
||||||
@@ -13,12 +14,15 @@ type CategorySelectProps = Partial<SelectProps> & {
|
|||||||
|
|
||||||
export function CategorySelect(props: CategorySelectProps) {
|
export function CategorySelect(props: CategorySelectProps) {
|
||||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
||||||
const categoriesById = categories?.reduce((map, c) => {
|
const categoriesById = categories?.reduce((map, c) => {
|
||||||
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) {
|
||||||
@@ -42,7 +46,7 @@ export function CategorySelect(props: CategorySelectProps) {
|
|||||||
.sort((c1, c2) => c1.label.localeCompare(c2.label))
|
.sort((c1, c2) => c1.label.localeCompare(c2.label))
|
||||||
if (props.withAll) {
|
if (props.withAll) {
|
||||||
selectData?.unshift({
|
selectData?.unshift({
|
||||||
label: t`All`,
|
label: _(msg`All`),
|
||||||
value: Constants.categories.all.id,
|
value: Constants.categories.all.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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"
|
||||||
@@ -11,10 +12,11 @@ import { TbFileImport } from "react-icons/tb"
|
|||||||
|
|
||||||
export function ImportOpml() {
|
export function ImportOpml() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const form = useForm<{ file: File }>({
|
const form = useForm<{ file: File }>({
|
||||||
validate: {
|
validate: {
|
||||||
file: isNotEmpty(t`OPML file is required`),
|
file: isNotEmpty(_(msg`OPML file is required`)),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ export function ImportOpml() {
|
|||||||
<FileInput
|
<FileInput
|
||||||
label={<Trans>OPML file</Trans>}
|
label={<Trans>OPML file</Trans>}
|
||||||
leftSection={<TbFileImport />}
|
leftSection={<TbFileImport />}
|
||||||
placeholder={t`OPML file`}
|
placeholder={_(msg`OPML file`)}
|
||||||
description={
|
description={
|
||||||
<Trans>
|
<Trans>
|
||||||
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your
|
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your
|
||||||
|
|||||||
@@ -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,5 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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"
|
||||||
@@ -57,10 +58,11 @@ export function Header() {
|
|||||||
const searchFromStore = useAppSelector(state => state.entries.search)
|
const searchFromStore = useAppSelector(state => state.entries.search)
|
||||||
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
|
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const searchForm = useForm<{ search: string }>({
|
const searchForm = useForm<{ search: string }>({
|
||||||
validate: {
|
validate: {
|
||||||
search: value => (value.length > 0 && value.length < 3 ? t`Search requires at least 3 characters` : null),
|
search: value => (value.length > 0 && value.length < 3 ? _(msg`Search requires at least 3 characters`) : null),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { setValues } = searchForm
|
const { setValues } = searchForm
|
||||||
@@ -77,7 +79,7 @@ export function Header() {
|
|||||||
<HeaderToolbar>
|
<HeaderToolbar>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbArrowUp size={iconSize} />}
|
icon={<TbArrowUp size={iconSize} />}
|
||||||
label={<Trans>Previous</Trans>}
|
label={msg`Previous`}
|
||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
await dispatch(
|
await dispatch(
|
||||||
selectPreviousEntry({
|
selectPreviousEntry({
|
||||||
@@ -90,7 +92,7 @@ export function Header() {
|
|||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbArrowDown size={iconSize} />}
|
icon={<TbArrowDown size={iconSize} />}
|
||||||
label={<Trans>Next</Trans>}
|
label={msg`Next`}
|
||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
await dispatch(
|
await dispatch(
|
||||||
selectNextEntry({
|
selectNextEntry({
|
||||||
@@ -106,7 +108,7 @@ export function Header() {
|
|||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbRefresh size={iconSize} />}
|
icon={<TbRefresh size={iconSize} />}
|
||||||
label={<Trans>Refresh</Trans>}
|
label={msg`Refresh`}
|
||||||
onClick={async () => await dispatch(reloadEntries())}
|
onClick={async () => await dispatch(reloadEntries())}
|
||||||
/>
|
/>
|
||||||
<MarkAllAsReadButton iconSize={iconSize} />
|
<MarkAllAsReadButton iconSize={iconSize} />
|
||||||
@@ -115,25 +117,25 @@ export function Header() {
|
|||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
|
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
|
||||||
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
|
label={settings.readingMode === "all" ? msg`All` : msg`Unread`}
|
||||||
onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
|
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
|
||||||
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
|
label={settings.readingOrder === "asc" ? msg`Asc` : msg`Desc`}
|
||||||
onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Indicator disabled={!searchFromStore}>
|
<Indicator disabled={!searchFromStore}>
|
||||||
<ActionButton icon={<TbSearch size={iconSize} />} label={<Trans>Search</Trans>} />
|
<ActionButton icon={<TbSearch size={iconSize} />} label={msg`Search`} />
|
||||||
</Indicator>
|
</Indicator>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
|
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t`Search`}
|
placeholder={_(msg`Search`)}
|
||||||
{...searchForm.getInputProps("search")}
|
{...searchForm.getInputProps("search")}
|
||||||
leftSection={<TbSearch size={iconSize} />}
|
leftSection={<TbSearch size={iconSize} />}
|
||||||
rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
|
rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
|
||||||
@@ -153,12 +155,12 @@ export function Header() {
|
|||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbSettings size={iconSize} />}
|
icon={<TbSettings size={iconSize} />}
|
||||||
label={<Trans>Extension options</Trans>}
|
label={msg`Extension options`}
|
||||||
onClick={() => openSettingsPage()}
|
onClick={() => openSettingsPage()}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbExternalLink size={iconSize} />}
|
icon={<TbExternalLink size={iconSize} />}
|
||||||
label={<Trans>Open CommaFeed</Trans>}
|
label={msg`Open CommaFeed`}
|
||||||
onClick={() => openAppInNewTab()}
|
onClick={() => openAppInNewTab()}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans, msg } from "@lingui/macro"
|
||||||
|
|
||||||
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
||||||
import { markAllEntries } from "app/entries/thunks"
|
import { markAllEntries } from "app/entries/thunks"
|
||||||
@@ -91,7 +91,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
<ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
|
<ActionButton icon={<TbChecks size={props.iconSize} />} label={msg`Mark all as read`} onClick={buttonClicked} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,10 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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 +19,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)
|
||||||
@@ -33,6 +34,7 @@ export function DisplaySettings() {
|
|||||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const scrollModeOptions: Record<ScrollMode, ReactNode> = {
|
const scrollModeOptions: Record<ScrollMode, ReactNode> = {
|
||||||
always: <Trans>Always</Trans>,
|
always: <Trans>Always</Trans>,
|
||||||
@@ -43,19 +45,19 @@ export function DisplaySettings() {
|
|||||||
const displayModeData: ComboboxData = [
|
const displayModeData: ComboboxData = [
|
||||||
{
|
{
|
||||||
value: "always",
|
value: "always",
|
||||||
label: t`Always`,
|
label: _(msg`Always`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "on_desktop",
|
value: "on_desktop",
|
||||||
label: t`On desktop`,
|
label: _(msg`On desktop`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "on_mobile",
|
value: "on_mobile",
|
||||||
label: t`On mobile`,
|
label: _(msg`On mobile`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "never",
|
value: "never",
|
||||||
label: t`Never`,
|
label: _(msg`Never`),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
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"
|
||||||
@@ -19,10 +20,11 @@ interface FormData extends ProfileModificationRequest {
|
|||||||
export function ProfileSettings() {
|
export function ProfileSettings() {
|
||||||
const profile = useAppSelector(state => state.user.profile)
|
const profile = useAppSelector(state => state.user.profile)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
validate: {
|
validate: {
|
||||||
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? t`Passwords do not match` : null),
|
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? _(msg`Passwords do not match`) : null),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { setValues } = form
|
const { setValues } = form
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -70,6 +70,7 @@ export function Tree() {
|
|||||||
const allCategoryNode = () => (
|
const allCategoryNode = () => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
id={Constants.categories.all.id}
|
id={Constants.categories.all.id}
|
||||||
|
type="category"
|
||||||
name={<Trans>All</Trans>}
|
name={<Trans>All</Trans>}
|
||||||
icon={allIcon}
|
icon={allIcon}
|
||||||
unread={categoryUnreadCount(root)}
|
unread={categoryUnreadCount(root)}
|
||||||
@@ -83,6 +84,7 @@ export function Tree() {
|
|||||||
const starredCategoryNode = () => (
|
const starredCategoryNode = () => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
id={Constants.categories.starred.id}
|
id={Constants.categories.starred.id}
|
||||||
|
type="category"
|
||||||
name={<Trans>Starred</Trans>}
|
name={<Trans>Starred</Trans>}
|
||||||
icon={starredIcon}
|
icon={starredIcon}
|
||||||
unread={0}
|
unread={0}
|
||||||
@@ -102,6 +104,7 @@ export function Tree() {
|
|||||||
return (
|
return (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
id={category.id}
|
id={category.id}
|
||||||
|
type="category"
|
||||||
name={category.name}
|
name={category.name}
|
||||||
icon={category.expanded ? expandedIcon : collapsedIcon}
|
icon={category.expanded ? expandedIcon : collapsedIcon}
|
||||||
unread={unreadCount}
|
unread={unreadCount}
|
||||||
@@ -122,6 +125,7 @@ export function Tree() {
|
|||||||
return (
|
return (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
id={String(feed.id)}
|
id={String(feed.id)}
|
||||||
|
type="feed"
|
||||||
name={feed.name}
|
name={feed.name}
|
||||||
icon={feed.iconUrl}
|
icon={feed.iconUrl}
|
||||||
unread={feed.unread}
|
unread={feed.unread}
|
||||||
@@ -137,6 +141,7 @@ export function Tree() {
|
|||||||
const tagNode = (tag: string) => (
|
const tagNode = (tag: string) => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
id={tag}
|
id={tag}
|
||||||
|
type="tag"
|
||||||
name={tag}
|
name={tag}
|
||||||
icon={tagIcon}
|
icon={tagIcon}
|
||||||
unread={0}
|
unread={0}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Box, Center } from "@mantine/core"
|
import { Box, Center } from "@mantine/core"
|
||||||
|
import type { EntrySourceType } from "app/entries/slice"
|
||||||
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
|
type: EntrySourceType
|
||||||
icon: ReactNode
|
name: React.ReactNode
|
||||||
|
icon: React.ReactNode
|
||||||
unread: number
|
unread: number
|
||||||
selected: boolean
|
selected: boolean
|
||||||
expanded?: boolean
|
expanded?: boolean
|
||||||
@@ -27,7 +29,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") {
|
||||||
@@ -63,7 +65,15 @@ export function TreeNode(props: TreeNodeProps) {
|
|||||||
hasUnread: props.unread > 0,
|
hasUnread: props.unread > 0,
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
|
<Box
|
||||||
|
py={1}
|
||||||
|
pl={props.level * 20}
|
||||||
|
className={classes.node}
|
||||||
|
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
|
||||||
|
data-id={props.id}
|
||||||
|
data-type={props.type}
|
||||||
|
data-unread-count={props.unread}
|
||||||
|
>
|
||||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
|
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
|
||||||
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
|
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { Trans, msg } from "@lingui/macro"
|
||||||
import { Box, Center, Kbd, TextInput } from "@mantine/core"
|
import { useLingui } from "@lingui/react"
|
||||||
import { Spotlight, spotlight, type SpotlightActionData } from "@mantine/spotlight"
|
import { TextInput } from "@mantine/core"
|
||||||
|
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,6 +15,7 @@ export interface TreeSearchProps {
|
|||||||
|
|
||||||
export function TreeSearch(props: TreeSearchProps) {
|
export function TreeSearch(props: TreeSearchProps) {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const actions: SpotlightActionData[] = props.feeds
|
const actions: SpotlightActionData[] = props.feeds
|
||||||
.map(f => ({
|
.map(f => ({
|
||||||
@@ -25,13 +27,6 @@ export function TreeSearch(props: TreeSearchProps) {
|
|||||||
.sort((f1, f2) => f1.label.localeCompare(f2.label))
|
.sort((f1, f2) => f1.label.localeCompare(f2.label))
|
||||||
|
|
||||||
const searchIcon = <TbSearch size={18} />
|
const searchIcon = <TbSearch size={18} />
|
||||||
const rightSection = (
|
|
||||||
<Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
|
|
||||||
<Kbd>Ctrl</Kbd>
|
|
||||||
<Box mx={5}>+</Box>
|
|
||||||
<Kbd>K</Kbd>
|
|
||||||
</Center>
|
|
||||||
)
|
|
||||||
|
|
||||||
// additional keyboard shortcut used by commafeed v1
|
// additional keyboard shortcut used by commafeed v1
|
||||||
useMousetrap("g u", () => spotlight.open())
|
useMousetrap("g u", () => spotlight.open())
|
||||||
@@ -39,10 +34,9 @@ export function TreeSearch(props: TreeSearchProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t`Search`}
|
placeholder={_(msg`Search`)}
|
||||||
leftSection={searchIcon}
|
leftSection={searchIcon}
|
||||||
rightSectionWidth={100}
|
rightSectionWidth={100}
|
||||||
rightSection={rightSection}
|
|
||||||
styles={{
|
styles={{
|
||||||
input: {
|
input: {
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
@@ -56,13 +50,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: _(msg`Search`),
|
||||||
}}
|
}}
|
||||||
nothingFound={<Trans>Nothing found</Trans>}
|
nothingFound={<Trans>Nothing found</Trans>}
|
||||||
></Spotlight>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function UnreadCount(props: { unreadCount: number }) {
|
|||||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||||
return (
|
return (
|
||||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||||
<Badge className={classes.badge} variant="light">
|
<Badge className={classes.badge} variant="light" fullWidth>
|
||||||
{count}
|
{count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { msg } from "@lingui/macro"
|
||||||
|
import { useLingui } from "@lingui/react"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
|
|
||||||
interface Step {
|
interface Step {
|
||||||
@@ -11,22 +12,23 @@ export const useAppLoading = () => {
|
|||||||
const settings = useAppSelector(state => state.user.settings)
|
const settings = useAppSelector(state => state.user.settings)
|
||||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||||
const tags = useAppSelector(state => state.user.tags)
|
const tags = useAppSelector(state => state.user.tags)
|
||||||
|
const { _ } = useLingui()
|
||||||
|
|
||||||
const steps: Step[] = [
|
const steps: Step[] = [
|
||||||
{
|
{
|
||||||
label: t`Loading settings...`,
|
label: _(msg`Loading settings...`),
|
||||||
done: !!settings,
|
done: !!settings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t`Loading profile...`,
|
label: _(msg`Loading profile...`),
|
||||||
done: !!profile,
|
done: !!profile,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t`Loading subscriptions...`,
|
label: _(msg`Loading subscriptions...`),
|
||||||
done: !!rootCategory,
|
done: !!rootCategory,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t`Loading tags...`,
|
label: _(msg`Loading tags...`),
|
||||||
done: !!tags,
|
done: !!tags,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -10,7 +10,7 @@ interface Locale {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add an object to the array to add a new locale
|
// add an object to the array to add a new locale
|
||||||
// don't forget to also add it to the 'locales' array in .linguirc
|
// don't forget to also add it to the 'locales' array in lingui.config.ts
|
||||||
export const locales: Locale[] = [
|
export const locales: Locale[] = [
|
||||||
{ key: "ar", label: "العربية", dayjsImportFn: async () => await import("dayjs/locale/ar") },
|
{ key: "ar", label: "العربية", dayjsImportFn: async () => await import("dayjs/locale/ar") },
|
||||||
{ key: "ca", label: "Català", dayjsImportFn: async () => await import("dayjs/locale/ca") },
|
{ key: "ca", label: "Català", dayjsImportFn: async () => await import("dayjs/locale/ca") },
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "تأكد من عمل الخلاصة"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Comproveu que el canal funciona"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr "Tanca el menu"
|
msgstr "Tanca el menu"
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}."
|
msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}."
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Zkontrolujte, zda zdroj funguje"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Gwiriwch fod y porthiant yn gweithio"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Tjek, at foderet virker"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Überprüfe, ob der Feed funktioniert"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr "Menü schließen"
|
msgstr "Menü schließen"
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}."
|
msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}."
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Check that the feed is working"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr "Close menu"
|
msgstr "Close menu"
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr "Cmd"
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
|
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Compruebe que el feed funciona"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "بررسی کنید که خوراک کار می کند"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Tarkista, että syöttö toimii"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Vérifie que le flux fonctionne"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr "Fermer le menu"
|
msgstr "Fermer le menu"
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
|
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Comproba que a fonte funciona"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Ellenőrizze, hogy a feed működik-e"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Periksa apakah umpannya berfungsi"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Verifica che il feed funzioni"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "フィードが動作していることを確認してください"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "피드가 작동하는지 확인"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Semak sama ada suapan berfungsi"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Sjekk at feeden fungerer"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Controleer of de feed werkt"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Sjekk at feeden fungerer"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Sprawdź, czy kanał działa"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Verifique se o feed está funcionando"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Проверьте, работает ли лента."
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr "Закрыть меню"
|
msgstr "Закрыть меню"
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr "Версия расширения браузера CommaFeed {browserExtensionVersion}."
|
msgstr "Версия расширения браузера CommaFeed {browserExtensionVersion}."
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Skontrolujte, či feed funguje"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ msgstr "Kontrollera att matningen fungerar"
|
|||||||
msgid "Close menu"
|
msgid "Close menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
|
msgid "Cmd"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/app/AboutPage.tsx
|
#: src/pages/app/AboutPage.tsx
|
||||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user