forked from Archives/Athou_commafeed
Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f0c33c1d | ||
|
|
563516901e | ||
|
|
73b40fd8b7 | ||
|
|
08224a8486 | ||
|
|
993f3d3aa8 | ||
|
|
3a975de136 | ||
|
|
48b5195798 | ||
|
|
8eb34c7539 | ||
|
|
d75d7a9209 | ||
|
|
ddf851f1eb | ||
|
|
889dd00c23 | ||
|
|
c5ea2a1aa1 | ||
|
|
1489aff78e | ||
|
|
640296d42f | ||
|
|
3b12b2a5f6 | ||
|
|
d5c41a5167 | ||
|
|
58bf86d25d | ||
|
|
d73034d6d9 | ||
|
|
151a613dcc | ||
|
|
4bb741a42f | ||
|
|
cc9c8d3db3 | ||
|
|
c3d4831550 | ||
|
|
31e385fbfb | ||
|
|
a8c47d717c | ||
|
|
9a25157d3f | ||
|
|
9176e0f7b7 | ||
|
|
ff7aa890a6 | ||
|
|
03312c1592 | ||
|
|
9d1ec2c636 | ||
|
|
c49c31a44e | ||
|
|
947c1f562f | ||
|
|
2d1dbb6988 | ||
|
|
622e46ff67 | ||
|
|
4ff45a65c3 | ||
|
|
a62676061b | ||
|
|
11d77d2265 | ||
|
|
1e7d44b250 | ||
|
|
ffd86c6d8c | ||
|
|
a566c9460d | ||
|
|
24edae3d58 | ||
|
|
97876344c4 | ||
|
|
95dbeb9a47 | ||
|
|
3fc64859b1 | ||
|
|
896fe3b5b2 | ||
|
|
85404781a3 | ||
|
|
efe2abc86e | ||
|
|
b70b7a0b40 | ||
|
|
865c80f87b | ||
|
|
23a91aab12 | ||
|
|
085a3cbb50 | ||
|
|
fb9d875c31 | ||
|
|
5ee15c6f68 | ||
|
|
9853205849 | ||
|
|
2c9ce7e8fc | ||
|
|
9753ae60e2 | ||
|
|
bd66f1e682 | ||
|
|
ed6a45c119 | ||
|
|
8f53ce27fc | ||
|
|
f7ae2e6689 | ||
|
|
c6cc47192c | ||
|
|
1c447fe369 | ||
|
|
6b5c92db48 | ||
|
|
427e020d27 | ||
|
|
18084995b2 | ||
|
|
f894fdf564 | ||
|
|
0b0a964a90 | ||
|
|
d6df979d0d | ||
|
|
c366c37afe | ||
|
|
20cbd239b2 | ||
|
|
a9c7595ee7 | ||
|
|
3f09e3ca64 | ||
|
|
ed42db7a0d | ||
|
|
c85daeb46e | ||
|
|
3f2b93f1f8 | ||
|
|
78d2e66c56 | ||
|
|
0f2de651ff | ||
|
|
2eb7c7237e | ||
|
|
3b8f62ff11 | ||
|
|
f8bf9370de | ||
|
|
30cd0ec089 | ||
|
|
e984be9289 | ||
|
|
8069787754 | ||
|
|
343e442dff | ||
|
|
313ccdeae9 | ||
|
|
fdec8ebfd3 | ||
|
|
efddd86263 | ||
|
|
7d18bde40b | ||
|
|
7fee410be4 | ||
|
|
ebc2516a53 | ||
|
|
ade4d1d782 | ||
|
|
07f7a288d2 | ||
|
|
380ed16caf | ||
|
|
db654a10d1 | ||
|
|
2cf84d35cd | ||
|
|
a4eac86913 | ||
|
|
5168be45a8 | ||
|
|
163ab43da3 | ||
|
|
e5fa517270 | ||
|
|
b8b8ea5ce2 | ||
|
|
991b147af5 | ||
|
|
ecff62d0fa | ||
|
|
cdec4c0879 | ||
|
|
e8085ac4cf | ||
|
|
327062112b | ||
|
|
6dfc23c33a | ||
|
|
a601b0ab35 | ||
|
|
a48c8ca87a | ||
|
|
b59e64a3d1 | ||
|
|
5fc62dd06d | ||
|
|
d81a0cae91 | ||
|
|
50e31c6b69 | ||
|
|
92d3d88127 | ||
|
|
517fdb2095 | ||
|
|
d16ebb02b4 | ||
|
|
a5c64c8b7b | ||
|
|
5287a93484 | ||
|
|
06e84d9032 | ||
|
|
3a63dd032a | ||
|
|
889e227523 | ||
|
|
57d895daf5 | ||
|
|
a744394faa | ||
|
|
64e3c25bad | ||
|
|
75f85e1fb2 | ||
|
|
00bd4cab37 | ||
|
|
a9527f59a9 | ||
|
|
77661930f0 | ||
|
|
80a09bd9a0 | ||
|
|
6eb7cfbdc2 | ||
|
|
fb186530aa | ||
|
|
6c121ccb90 | ||
|
|
c08ad3b365 | ||
|
|
1668bc88ad | ||
|
|
3a43f62460 | ||
|
|
bfba5179d1 | ||
|
|
78bf7856dc | ||
|
|
e0c708f677 | ||
|
|
794d6824e8 | ||
|
|
15573a7bee | ||
|
|
31c61a79c6 | ||
|
|
87ca427094 | ||
|
|
99bdc904e0 | ||
|
|
2fdee68feb | ||
|
|
7be014f83e | ||
|
|
5668fe0a33 | ||
|
|
32c07efe19 | ||
|
|
21b23d0f79 | ||
|
|
793d0dd13f | ||
|
|
14e8ff4c1b | ||
|
|
416ab06997 | ||
|
|
493cd60dae | ||
|
|
e0948e1e9e | ||
|
|
5776b8c044 | ||
|
|
38ab4105d8 | ||
|
|
5ed9dadcc2 | ||
|
|
357d7e2381 | ||
|
|
8cfaab3e9f | ||
|
|
fef2404357 | ||
|
|
1aa1bce8c8 | ||
|
|
124b2761f6 | ||
|
|
066ca1af7c | ||
|
|
c20520879b | ||
|
|
4fa5b2b856 | ||
|
|
5c1b1fad76 | ||
|
|
c18d248c06 | ||
|
|
d46ee7f673 | ||
|
|
f2c0d99bd9 | ||
|
|
60ee0b9185 | ||
|
|
4b3e660ae7 | ||
|
|
0b42392bfc | ||
|
|
a94d7ce235 | ||
|
|
72aec432ed | ||
|
|
0e5db8d604 | ||
|
|
dc45fb4b84 | ||
|
|
6503d38fe3 | ||
|
|
32c89d9a11 | ||
|
|
f279465750 | ||
|
|
58ec1b022a | ||
|
|
612199429e | ||
|
|
e5482f9051 | ||
|
|
05df14fda2 | ||
|
|
29898ba1ba | ||
|
|
93d1cec503 | ||
|
|
9884f44122 | ||
|
|
d400456685 | ||
|
|
c39069cafd | ||
|
|
5fb0edc318 | ||
|
|
21a6b2d780 | ||
|
|
40c9063a54 | ||
|
|
59b0103ed5 | ||
|
|
f4730e9338 | ||
|
|
b7b520ca3c | ||
|
|
21d44e6a55 | ||
|
|
607886f0f0 | ||
|
|
7cd3c68256 | ||
|
|
6e37c1bd86 | ||
|
|
5db1a0748f | ||
|
|
a7584df4f4 | ||
|
|
4421197403 | ||
|
|
15b59467fb | ||
|
|
c95ff0a2ce | ||
|
|
7eff9df025 | ||
|
|
2f05e53e14 | ||
|
|
6089fe4036 | ||
|
|
10d9af0d86 | ||
|
|
c119d5062a | ||
|
|
324609ee60 | ||
|
|
a0a65f2b45 | ||
|
|
45e5ca704c | ||
|
|
f361be0c72 | ||
|
|
1611dc5703 | ||
|
|
04faad84a4 | ||
|
|
19c42e5838 | ||
|
|
4918b69d0a | ||
|
|
c7cec464aa | ||
|
|
91857c4d73 | ||
|
|
fc6f9f4258 | ||
|
|
34f9f9374a | ||
|
|
0ae4c1621f | ||
|
|
c393f5c045 | ||
|
|
1624290dc1 | ||
|
|
c6491990ac | ||
|
|
15dea17923 | ||
|
|
689d5ac7b2 | ||
|
|
2142e20e7d | ||
|
|
dc23126570 | ||
|
|
55856f9060 | ||
|
|
c756ce5fc8 | ||
|
|
0546f25d55 | ||
|
|
7b33717333 | ||
|
|
6ea6d16e58 | ||
|
|
a9b65c83aa | ||
|
|
a497802b50 | ||
|
|
42b0428b9a | ||
|
|
931c553e1d | ||
|
|
f3c0b92a3c | ||
|
|
970cabf241 | ||
|
|
e321ecde5d | ||
|
|
32ac326a77 | ||
|
|
134dcd4466 | ||
|
|
26a44353d4 | ||
|
|
55acb3ef28 | ||
|
|
0e96307726 | ||
|
|
0199a36238 | ||
|
|
3f2f6e83fa | ||
|
|
4fa780cac2 | ||
|
|
edb0f655b0 | ||
|
|
651ada7073 | ||
|
|
efb5d49d04 | ||
|
|
f78cc18b06 | ||
|
|
8acffa11e5 | ||
|
|
f4246807ff | ||
|
|
abf6e7131b | ||
|
|
b2688520cc | ||
|
|
d6910aa1e8 | ||
|
|
afc56c6053 | ||
|
|
1bd504cbfb |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
# Setup
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@01ed653ac833fe80569f1ef9f25585ba2811baab # v1
|
||||
uses: graalvm/setup-graalvm@7f488cf82a3629ee755e4e97342c01d6bed318fa # v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
@@ -107,14 +107,14 @@ jobs:
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
|
||||
- name: Install required packages
|
||||
run: sudo apt-get install -y rename unzip
|
||||
|
||||
# Prepare artifacts
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
pattern: commafeed-${{ matrix.database }}-*
|
||||
path: ./artifacts
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
|
||||
## build but don't push for PRs and renovate
|
||||
- name: Docker build - native
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
- name: Docker build - jvm
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
## build and push tag
|
||||
- name: Docker build and push tag - native
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push tag - jvm
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
|
||||
## build and push master
|
||||
- name: Docker build and push master - native
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
tags: athou/commafeed:master-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push master - jvm
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
pattern: commafeed-*
|
||||
path: ./artifacts
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1
|
||||
uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
|
||||
78
.github/workflows/scorecard.yml
vendored
Normal file
78
.github/workflows/scorecard.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||
# by a third-party and are governed by separate terms of service, privacy
|
||||
# policy, and support documentation.
|
||||
|
||||
name: Scorecard supply-chain security
|
||||
on:
|
||||
# For Branch-Protection check. Only the default branch is supported. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||
branch_protection_rule:
|
||||
# To guarantee Maintained check is occasionally updated. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||
schedule:
|
||||
- cron: '42 13 * * 4'
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
|
||||
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results and get a badge (see publish_results below).
|
||||
id-token: write
|
||||
# Uncomment the permissions below if installing in a private repository.
|
||||
# contents: read
|
||||
# actions: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||
# - you are installing Scorecard on a *private* repository
|
||||
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
|
||||
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||
|
||||
# Public repositories:
|
||||
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||
# - Allows the repository to include the Scorecard badge.
|
||||
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories:
|
||||
# - `publish_results` will always be set to `false`, regardless
|
||||
# of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
|
||||
# file_mode: git
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
41
.github/workflows/sonar.yml
vendored
Normal file
41
.github/workflows/sonar.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: SonarQube
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened ]
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 21
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "temurin"
|
||||
cache: "maven"
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: sudo apt-get install -y libgbm1
|
||||
|
||||
# Run test coverage and SonarQube analysis
|
||||
- name: Analyze with SonarQube
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: mvn --batch-mode verify sonar:sonar -Dsonar.projectKey=Athou_commafeed
|
||||
2
.mvn/wrapper/maven-wrapper.properties
vendored
2
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -15,4 +15,4 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [5.11.0]
|
||||
|
||||
- Add an option to navigate to the next unread category/feed when marking all entries as read (#1807)
|
||||
- Google Analytics support has been removed
|
||||
|
||||
## [5.10.0]
|
||||
|
||||
- Add an indicator next to each feed's unread count in the tree to show when new entries are discovered while the app is open (#1762)
|
||||
- Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again
|
||||
- The aarch64 native executable now also works on the Raspberry Pi 5 (#1795)
|
||||
- Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are displayed (#1087)
|
||||
|
||||
## [5.9.0]
|
||||
|
||||
- A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757)
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you found a vulnerability that you deem too sensitive to disclose publicly in a Github issue, please send an email at jeremiepanzer at gmail dot com.
|
||||
Thanks !
|
||||
If you found a vulnerability that you deem too sensitive to disclose publicly in a Github issue, please create a private security advisory here: https://github.com/Athou/commafeed/security/advisories
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
@@ -13,15 +13,7 @@
|
||||
"arrowParentheses": "asNeeded"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUnusedImports": "error",
|
||||
"noUnusedVariables": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist", "node_modules", "target", "target-ide"]
|
||||
"includes": ["**", "!**/dist", "!**/node_modules", "!**/target", "!**/target-ide"]
|
||||
}
|
||||
}
|
||||
|
||||
3071
commafeed-client/package-lock.json
generated
3071
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,69 +16,68 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@fontsource/open-sans": "^5.2.5",
|
||||
"@lingui/core": "^5.3.1",
|
||||
"@lingui/react": "^5.3.1",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/form": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@mantine/modals": "^8.0.0",
|
||||
"@mantine/notifications": "^8.0.0",
|
||||
"@mantine/spotlight": "^8.0.0",
|
||||
"@fontsource/open-sans": "^5.2.6",
|
||||
"@lingui/core": "^5.4.0",
|
||||
"@lingui/react": "^5.4.0",
|
||||
"@mantine/core": "^8.2.3",
|
||||
"@mantine/form": "^8.2.3",
|
||||
"@mantine/hooks": "^8.2.3",
|
||||
"@mantine/modals": "^8.2.3",
|
||||
"@mantine/notifications": "^8.2.3",
|
||||
"@mantine/spotlight": "^8.2.3",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@reduxjs/toolkit": "^2.8.1",
|
||||
"axios": "^1.9.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"axios": "^1.11.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^19.1.0",
|
||||
"react": "^19.1.1",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"redoc": "^2.5.0",
|
||||
"style-to-object": "^1.0.8",
|
||||
"style-to-object": "^1.0.9",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tinycon": "^0.6.8",
|
||||
"tss-react": "^4.9.18",
|
||||
"tss-react": "^4.9.19",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.3.1",
|
||||
"@lingui/cli": "^5.3.1",
|
||||
"@lingui/vite-plugin": "^5.3.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@biomejs/biome": "^2.1.3",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.4.0",
|
||||
"@lingui/cli": "^5.4.0",
|
||||
"@lingui/vite-plugin": "^5.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-infinite-scroller": "^1.2.5",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycon": "^0.6.7",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-checker": "^0.9.3",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-checker": "^0.10.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.3"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"overrides": {
|
||||
"react-infinite-scroller": {
|
||||
"react": "^19.1.0"
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>5.9.0</version>
|
||||
<version>5.11.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
<properties>
|
||||
<sonar.sources>package.json,src</sonar.sources>
|
||||
<sonar.coverage.exclusions>**/*</sonar.coverage.exclusions>
|
||||
|
||||
<!-- renovate: datasource=node-version depName=node -->
|
||||
<node.version>v22.15.0</node.version>
|
||||
<node.version>v22.18.0</node.version>
|
||||
<!-- renovate: datasource=npm depName=npm -->
|
||||
<npm.version>11.3.0</npm.version>
|
||||
<npm.version>11.5.2</npm.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -3,40 +3,44 @@ import { I18nProvider } from "@lingui/react"
|
||||
import { MantineProvider } from "@mantine/core"
|
||||
import { ModalsProvider } from "@mantine/modals"
|
||||
import { Notifications } from "@mantine/notifications"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectTo } from "app/redirect/slice"
|
||||
import { reloadServerInfos } from "app/server/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { categoryUnreadCount } from "app/utils"
|
||||
import { DisablePullToRefresh } from "components/DisablePullToRefresh"
|
||||
import { ErrorBoundary } from "components/ErrorBoundary"
|
||||
import { Header } from "components/header/Header"
|
||||
import { Tree } from "components/sidebar/Tree"
|
||||
import { useAppLoading } from "hooks/useAppLoading"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useI18n } from "i18n"
|
||||
import { WelcomePage } from "pages/WelcomePage"
|
||||
import { AdminUsersPage } from "pages/admin/AdminUsersPage"
|
||||
import { MetricsPage } from "pages/admin/MetricsPage"
|
||||
import { AboutPage } from "pages/app/AboutPage"
|
||||
import { AddPage } from "pages/app/AddPage"
|
||||
import { CategoryDetailsPage } from "pages/app/CategoryDetailsPage"
|
||||
import { DonatePage } from "pages/app/DonatePage"
|
||||
import { FeedDetailsPage } from "pages/app/FeedDetailsPage"
|
||||
import { FeedEntriesPage } from "pages/app/FeedEntriesPage"
|
||||
import Layout from "pages/app/Layout"
|
||||
import { SettingsPage } from "pages/app/SettingsPage"
|
||||
import { TagDetailsPage } from "pages/app/TagDetailsPage"
|
||||
import { LoginPage } from "pages/auth/LoginPage"
|
||||
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
||||
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import type React from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { isSafari } from "react-device-detect"
|
||||
import ReactGA from "react-ga4"
|
||||
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
||||
import { HashRouter, Navigate, Route, Routes, useNavigate } from "react-router-dom"
|
||||
import Tinycon from "tinycon"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { redirectTo } from "@/app/redirect/slice"
|
||||
import { reloadServerInfos } from "@/app/server/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { categoryUnreadCount } from "@/app/utils"
|
||||
import { DisablePullToRefresh } from "@/components/DisablePullToRefresh"
|
||||
import { ErrorBoundary } from "@/components/ErrorBoundary"
|
||||
import { Header } from "@/components/header/Header"
|
||||
import { Tree } from "@/components/sidebar/Tree"
|
||||
import { useAppLoading } from "@/hooks/useAppLoading"
|
||||
import { useBrowserExtension } from "@/hooks/useBrowserExtension"
|
||||
import { useI18n } from "@/i18n"
|
||||
import { AdminUsersPage } from "@/pages/admin/AdminUsersPage"
|
||||
import { MetricsPage } from "@/pages/admin/MetricsPage"
|
||||
import { AboutPage } from "@/pages/app/AboutPage"
|
||||
import { AddPage } from "@/pages/app/AddPage"
|
||||
import { CategoryDetailsPage } from "@/pages/app/CategoryDetailsPage"
|
||||
import { DonatePage } from "@/pages/app/DonatePage"
|
||||
import { FeedDetailsPage } from "@/pages/app/FeedDetailsPage"
|
||||
import { FeedEntriesPage } from "@/pages/app/FeedEntriesPage"
|
||||
import Layout from "@/pages/app/Layout"
|
||||
import { SettingsPage } from "@/pages/app/SettingsPage"
|
||||
import { TagDetailsPage } from "@/pages/app/TagDetailsPage"
|
||||
import { LoginPage } from "@/pages/auth/LoginPage"
|
||||
import { PasswordRecoveryPage } from "@/pages/auth/PasswordRecoveryPage"
|
||||
import { RegistrationPage } from "@/pages/auth/RegistrationPage"
|
||||
import { WelcomePage } from "@/pages/WelcomePage"
|
||||
|
||||
function Providers(props: { children: React.ReactNode }) {
|
||||
function Providers(
|
||||
props: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>
|
||||
) {
|
||||
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
@@ -72,9 +76,6 @@ function Providers(props: { children: React.ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
// api documentation page is very large, load only on-demand
|
||||
const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage"))
|
||||
|
||||
function AppRoutes() {
|
||||
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
|
||||
|
||||
@@ -85,7 +86,6 @@ function AppRoutes() {
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="register" element={<RegistrationPage />} />
|
||||
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
|
||||
<Route path="api" element={<ApiDocumentationPage />} />
|
||||
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarVisible={sidebarVisible} />}>
|
||||
<Route path="category">
|
||||
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
|
||||
@@ -128,26 +128,19 @@ function RedirectHandler() {
|
||||
return null
|
||||
}
|
||||
|
||||
function GoogleAnalyticsHandler() {
|
||||
const location = useLocation()
|
||||
const googleAnalyticsCode = useAppSelector(state => state.server.serverInfos?.googleAnalyticsCode)
|
||||
|
||||
useEffect(() => {
|
||||
if (googleAnalyticsCode) ReactGA.initialize(googleAnalyticsCode)
|
||||
}, [googleAnalyticsCode])
|
||||
|
||||
useEffect(() => {
|
||||
if (ReactGA.isInitialized) ReactGA.send({ hitType: "pageview", page: location.pathname })
|
||||
}, [location])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
function UnreadCountTitleHandler({
|
||||
enabled,
|
||||
}: Readonly<{
|
||||
enabled?: boolean
|
||||
}>) {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title>
|
||||
}
|
||||
|
||||
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
function UnreadCountFaviconHandler({ enabled }: { enabled?: boolean }) {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
useEffect(() => {
|
||||
if (enabled && unreadCount > 0) {
|
||||
Tinycon.setBubble(unreadCount)
|
||||
@@ -205,38 +198,32 @@ function CustomCssHandler() {
|
||||
|
||||
export function App() {
|
||||
useI18n()
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(reloadServerInfos())
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<>
|
||||
<UnreadCountTitleHandler unreadCount={unreadCount} enabled={unreadCountTitle} />
|
||||
<UnreadCountFaviconHandler unreadCount={unreadCount} enabled={unreadCountFavicon} />
|
||||
<BrowserExtensionBadgeUnreadCountHandler />
|
||||
<CustomJsHandler />
|
||||
<CustomCssHandler />
|
||||
<UnreadCountTitleHandler enabled={unreadCountTitle} />
|
||||
<UnreadCountFaviconHandler enabled={unreadCountFavicon} />
|
||||
<BrowserExtensionBadgeUnreadCountHandler />
|
||||
<CustomJsHandler />
|
||||
<CustomCssHandler />
|
||||
|
||||
{/* disable pull-to-refresh as it messes with vertical scrolling
|
||||
{/* 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 />}
|
||||
{!isSafari && <DisablePullToRefresh />}
|
||||
|
||||
<HashRouter>
|
||||
<GoogleAnalyticsHandler />
|
||||
<RedirectHandler />
|
||||
<AppRoutes />
|
||||
</HashRouter>
|
||||
</>
|
||||
<HashRouter>
|
||||
<RedirectHandler />
|
||||
<AppRoutes />
|
||||
</HashRouter>
|
||||
</Providers>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import type { AppDispatch, RootState } from "app/store"
|
||||
import type { AppDispatch, RootState } from "@/app/store"
|
||||
|
||||
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
||||
state: RootState
|
||||
|
||||
@@ -105,7 +105,7 @@ export const client = {
|
||||
},
|
||||
admin: {
|
||||
getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"),
|
||||
saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req),
|
||||
saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post<number>("admin/user/save", req),
|
||||
deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req),
|
||||
getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"),
|
||||
},
|
||||
|
||||
@@ -87,17 +87,15 @@ export const Constants = {
|
||||
headerHeight: 60,
|
||||
entryMaxWidth: 650,
|
||||
isTopVisible: (div: HTMLElement) => {
|
||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||
const header = document.getElementsByTagName("header").item(0)?.getBoundingClientRect()
|
||||
return div.getBoundingClientRect().top >= (header?.bottom ?? 0)
|
||||
},
|
||||
isBottomVisible: (div: HTMLElement) => {
|
||||
const footer = document.getElementById(Constants.dom.footerId)?.getBoundingClientRect()
|
||||
const footer = document.getElementsByTagName("footer").item(0)?.getBoundingClientRect()
|
||||
return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight)
|
||||
},
|
||||
},
|
||||
dom: {
|
||||
headerId: "header",
|
||||
footerId: "footer",
|
||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||
entryContextMenuId: (entry: Entry) => entry.id,
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
|
||||
import { type RootState, reducers } from "app/store"
|
||||
import type { Entries, Entry } from "app/types"
|
||||
import type { AxiosResponse } from "axios"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { client } from "@/app/client"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "@/app/entries/thunks"
|
||||
import { type RootState, reducers } from "@/app/store"
|
||||
import type { Entries, Entry } from "@/app/types"
|
||||
|
||||
vi.mock(import("app/client"))
|
||||
vi.mock(import("@/app/client"))
|
||||
|
||||
describe("entries", () => {
|
||||
beforeEach(() => {
|
||||
@@ -27,7 +27,12 @@ describe("entries", () => {
|
||||
} as AxiosResponse<Entries>)
|
||||
|
||||
const store = configureStore({ reducer: reducers })
|
||||
const promise = store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true }))
|
||||
const promise = store.dispatch(
|
||||
loadEntries({
|
||||
source: { type: "feed", id: "feed-id" },
|
||||
clearSearch: true,
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState().entries.source.type).toBe("feed")
|
||||
expect(store.getState().entries.source.id).toBe("feed-id")
|
||||
@@ -130,11 +135,19 @@ describe("entries", () => {
|
||||
} as RootState,
|
||||
})
|
||||
|
||||
store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } }))
|
||||
store.dispatch(
|
||||
markAllEntries({
|
||||
sourceType: "category",
|
||||
req: { id: "all", read: true },
|
||||
})
|
||||
)
|
||||
expect(store.getState().entries.entries).toStrictEqual([
|
||||
{ id: "3", read: true },
|
||||
{ id: "4", read: true },
|
||||
])
|
||||
expect(client.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true })
|
||||
expect(client.category.markEntries).toHaveBeenCalledWith({
|
||||
id: "all",
|
||||
read: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { Constants } from "app/constants"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks"
|
||||
import type { Entry } from "app/types"
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "@/app/entries/thunks"
|
||||
import type { Entry } from "@/app/types"
|
||||
|
||||
export type EntrySourceType = "category" | "feed" | "tag"
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { type EntrySource, type EntrySourceType, entriesSlice, setMarkAllAsReadConfirmationDialogOpen, setSearch } from "app/entries/slice"
|
||||
import type { RootState } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import type { Entry, MarkRequest, TagRequest } from "app/types"
|
||||
import { reloadTags } from "app/user/thunks"
|
||||
import { scrollToWithCallback } from "app/utils"
|
||||
import { flushSync } from "react-dom"
|
||||
import { createAppAsyncThunk } from "@/app/async-thunk"
|
||||
import { client } from "@/app/client"
|
||||
import { Constants } from "@/app/constants"
|
||||
import {
|
||||
type EntrySource,
|
||||
type EntrySourceType,
|
||||
entriesSlice,
|
||||
setMarkAllAsReadConfirmationDialogOpen,
|
||||
setSearch,
|
||||
} from "@/app/entries/slice"
|
||||
import type { RootState } from "@/app/store"
|
||||
import { reloadTree, selectNextUnreadTreeItem } from "@/app/tree/thunks"
|
||||
import type { Entry, MarkRequest, TagRequest } from "@/app/types"
|
||||
import { reloadTags } from "@/app/user/thunks"
|
||||
import { scrollToWithCallback } from "@/app/utils"
|
||||
|
||||
const getEndpoint = (sourceType: EntrySourceType) =>
|
||||
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
|
||||
@@ -130,11 +136,12 @@ export const markAllAsReadWithConfirmationIfRequired = createAppAsyncThunk(
|
||||
const source = state.entries.source
|
||||
const entriesTimestamp = state.entries.timestamp ?? Date.now()
|
||||
const markAllAsReadConfirmation = state.user.settings?.markAllAsReadConfirmation
|
||||
const markAllAsReadNavigateToNextUnread = state.user.settings?.markAllAsReadNavigateToNextUnread
|
||||
|
||||
if (markAllAsReadConfirmation) {
|
||||
thunkApi.dispatch(setMarkAllAsReadConfirmationDialogOpen(true))
|
||||
} else {
|
||||
thunkApi.dispatch(
|
||||
await thunkApi.dispatch(
|
||||
markAllEntries({
|
||||
sourceType: source.type,
|
||||
req: {
|
||||
@@ -145,6 +152,9 @@ export const markAllAsReadWithConfirmationIfRequired = createAppAsyncThunk(
|
||||
},
|
||||
})
|
||||
)
|
||||
const isAllCategorySelected = source.type === "category" && source.id === Constants.categories.all.id
|
||||
if (markAllAsReadNavigateToNextUnread && !isAllCategorySelected)
|
||||
await thunkApi.dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -230,7 +240,7 @@ export const selectEntry = createAppAsyncThunk(
|
||||
)
|
||||
|
||||
const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||
const header = document.getElementsByTagName("header").item(0)?.getBoundingClientRect()
|
||||
const offset = (header?.bottom ?? 0) + margin
|
||||
scrollToWithCallback({
|
||||
options: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { redirectToCategory } from "app/redirect/thunks"
|
||||
import { store } from "app/store"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { redirectToCategory } from "@/app/redirect/thunks"
|
||||
import { store } from "@/app/store"
|
||||
|
||||
describe("redirects", () => {
|
||||
it("redirects to category", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
|
||||
interface RedirectState {
|
||||
to?: string
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectTo } from "app/redirect/slice"
|
||||
import { createAppAsyncThunk } from "@/app/async-thunk"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { redirectTo } from "@/app/redirect/slice"
|
||||
|
||||
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
|
||||
|
||||
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
|
||||
|
||||
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
|
||||
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", () => {
|
||||
window.location.href = "api-documentation/"
|
||||
})
|
||||
|
||||
export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
|
||||
const { source } = thunkApi.getState().entries
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { reloadServerInfos } from "app/server/thunks"
|
||||
import type { ServerInfo } from "app/types"
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { reloadServerInfos } from "@/app/server/thunks"
|
||||
import type { ServerInfo } from "@/app/types"
|
||||
|
||||
interface ServerState {
|
||||
serverInfos?: ServerInfo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { createAppAsyncThunk } from "@/app/async-thunk"
|
||||
import { client } from "@/app/client"
|
||||
|
||||
export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { entriesSlice } from "app/entries/slice"
|
||||
import { redirectSlice } from "app/redirect/slice"
|
||||
import { serverSlice } from "app/server/slice"
|
||||
import { treeSlice } from "app/tree/slice"
|
||||
import type { LocalSettings } from "app/types"
|
||||
import { initialLocalSettings, userSlice } from "app/user/slice"
|
||||
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
import { entriesSlice } from "@/app/entries/slice"
|
||||
import { redirectSlice } from "@/app/redirect/slice"
|
||||
import { serverSlice } from "@/app/server/slice"
|
||||
import { treeSlice } from "@/app/tree/slice"
|
||||
import type { LocalSettings } from "@/app/types"
|
||||
import { initialLocalSettings, userSlice } from "@/app/user/slice"
|
||||
|
||||
export const reducers = {
|
||||
entries: entriesSlice.reducer,
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { markEntry } from "app/entries/thunks"
|
||||
import { redirectTo } from "app/redirect/slice"
|
||||
import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
|
||||
import type { Category } from "app/types"
|
||||
import { visitCategoryTree } from "app/utils"
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { loadEntries, markEntry } from "@/app/entries/thunks"
|
||||
import { redirectTo } from "@/app/redirect/slice"
|
||||
import { collapseTreeCategory, reloadTree } from "@/app/tree/thunks"
|
||||
import type { Category, Subscription } from "@/app/types"
|
||||
import { flattenCategoryTree, visitCategoryTree } from "@/app/utils"
|
||||
|
||||
export interface TreeSubscription extends Subscription {
|
||||
// client-side only flag
|
||||
hasNewEntries?: boolean
|
||||
}
|
||||
|
||||
export interface TreeCategory extends Category {
|
||||
feeds: TreeSubscription[]
|
||||
children: TreeCategory[]
|
||||
}
|
||||
|
||||
interface TreeState {
|
||||
rootCategory?: Category
|
||||
rootCategory?: TreeCategory
|
||||
mobileMenuOpen: boolean
|
||||
sidebarVisible: boolean
|
||||
}
|
||||
@@ -37,12 +47,27 @@ export const treeSlice = createSlice({
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) {
|
||||
f.unread += action.payload.amount
|
||||
f.hasNewEntries = true
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadTree.fulfilled, (state, action) => {
|
||||
// set hasNewEntries to true if new unread > previous unread
|
||||
if (state.rootCategory) {
|
||||
const oldFeeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
|
||||
const oldFeedsById = new Map(oldFeeds.map(f => [f.id, f]))
|
||||
|
||||
const newFeeds = flattenCategoryTree(action.payload).flatMap(c => c.feeds)
|
||||
for (const newFeed of newFeeds) {
|
||||
const oldFeed = oldFeedsById.get(newFeed.id)
|
||||
if (oldFeed && newFeed.unread > oldFeed.unread) {
|
||||
newFeed.hasNewEntries = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.rootCategory = action.payload
|
||||
})
|
||||
builder.addCase(collapseTreeCategory.pending, (state, action) => {
|
||||
@@ -59,6 +84,25 @@ export const treeSlice = createSlice({
|
||||
}
|
||||
})
|
||||
})
|
||||
builder.addCase(loadEntries.fulfilled, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
|
||||
const { source } = action.meta.arg
|
||||
if (source.type === "category") {
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
if (c.id === source.id) {
|
||||
for (const f of flattenCategoryTree(c).flatMap(c => c.feeds)) {
|
||||
f.hasNewEntries = false
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (source.type === "feed") {
|
||||
const feeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
|
||||
for (const f of feeds.filter(f => f.id === +source.id)) {
|
||||
f.hasNewEntries = false
|
||||
}
|
||||
}
|
||||
})
|
||||
builder.addCase(redirectTo, state => {
|
||||
state.mobileMenuOpen = false
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { redirectToCategory, redirectToFeed } from "app/redirect/thunks"
|
||||
import { incrementUnreadCount } from "app/tree/slice"
|
||||
import type { CollapseRequest, Subscription } from "app/types"
|
||||
import { flattenCategoryTree, visitCategoryTree } from "app/utils"
|
||||
import { createAppAsyncThunk } from "@/app/async-thunk"
|
||||
import { client } from "@/app/client"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { redirectToCategory, redirectToFeed } from "@/app/redirect/thunks"
|
||||
import { incrementUnreadCount } from "@/app/tree/slice"
|
||||
import type { CollapseRequest, Subscription } from "@/app/types"
|
||||
import { flattenCategoryTree, visitCategoryTree } from "@/app/utils"
|
||||
|
||||
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
|
||||
|
||||
@@ -53,6 +54,9 @@ export const selectNextUnreadTreeItem = createAppAsyncThunk(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// redirect to 'all' if no unread categories or feeds found or if we reached the end of the list
|
||||
thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { type RootState, reducers } from "app/store"
|
||||
import { selectNextUnreadTreeItem } from "app/tree/thunks"
|
||||
import type { Category, Subscription } from "app/types"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import type { AxiosResponse } from "axios"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { client } from "@/app/client"
|
||||
import { loadEntries } from "@/app/entries/thunks"
|
||||
import { type RootState, reducers } from "@/app/store"
|
||||
import { newFeedEntriesDiscovered, selectNextUnreadTreeItem } from "@/app/tree/thunks"
|
||||
import type { Category, Entries, Entry, Subscription } from "@/app/types"
|
||||
|
||||
vi.mock(import("@/app/client"))
|
||||
|
||||
const createCategory = (id: string): Category => ({
|
||||
id,
|
||||
@@ -117,3 +122,51 @@ describe("selectNextUnreadTreeItem", () => {
|
||||
expect(store.getState().redirect.to).toBe("/app/feed/3")
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasNewEntries", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("sets and clear flag for a feed", async () => {
|
||||
vi.mocked(client.feed.getEntries).mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: false,
|
||||
name: "my-feed",
|
||||
errorCount: 3,
|
||||
feedLink: "https://mysite.com/feed",
|
||||
timestamp: 123,
|
||||
ignoredReadStatus: false,
|
||||
},
|
||||
} as AxiosResponse<Entries>)
|
||||
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
tree: {
|
||||
rootCategory: root,
|
||||
},
|
||||
entries: {
|
||||
source: {
|
||||
type: "feed",
|
||||
id: "1",
|
||||
},
|
||||
},
|
||||
} as RootState,
|
||||
})
|
||||
|
||||
// initial state
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].unread).toBe(0)
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBeFalsy()
|
||||
|
||||
// increments unread count and sets hasNewEntries to true
|
||||
await store.dispatch(newFeedEntriesDiscovered({ feedId: 1, amount: 3 }))
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].unread).toBe(3)
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBe(true)
|
||||
|
||||
// reload entries and sets hasNewEntries to false
|
||||
await store.dispatch(loadEntries({ source: { type: "feed", id: "1" }, clearSearch: true }))
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -214,7 +214,6 @@ export interface ServerInfo {
|
||||
version: string
|
||||
gitCommit: string
|
||||
allowRegistrations: boolean
|
||||
googleAnalyticsCode?: string
|
||||
smtpEnabled: boolean
|
||||
demoAccountEnabled: boolean
|
||||
websocketEnabled: boolean
|
||||
@@ -248,6 +247,7 @@ export interface Settings {
|
||||
starIconDisplayMode: IconDisplayMode
|
||||
externalLinkIconDisplayMode: IconDisplayMode
|
||||
markAllAsReadConfirmation: boolean
|
||||
markAllAsReadNavigateToNextUnread: boolean
|
||||
customContextMenu: boolean
|
||||
mobileFooter: boolean
|
||||
unreadCountTitle: boolean
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { showNotification } from "@mantine/notifications"
|
||||
import { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import type { LocalSettings, Settings, UserModel, ViewMode } from "app/types"
|
||||
import { createSlice, isAnyOf, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import type { LocalSettings, Settings, UserModel, ViewMode } from "@/app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMarkAllAsReadNavigateToUnread,
|
||||
changeMobileFooter,
|
||||
changePrimaryColor,
|
||||
changeReadingMode,
|
||||
@@ -114,6 +115,10 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.markAllAsReadConfirmation = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeMarkAllAsReadNavigateToUnread.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.markAllAsReadNavigateToNextUnread = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeCustomContextMenu.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.customContextMenu = action.meta.arg
|
||||
@@ -149,6 +154,7 @@ export const userSlice = createSlice({
|
||||
changeStarIconDisplayMode.fulfilled,
|
||||
changeExternalLinkIconDisplayMode.fulfilled,
|
||||
changeMarkAllAsReadConfirmation.fulfilled,
|
||||
changeMarkAllAsReadNavigateToUnread.fulfilled,
|
||||
changeCustomContextMenu.fulfilled,
|
||||
changeMobileFooter.fulfilled,
|
||||
changeUnreadCountTitle.fulfilled,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { reloadEntries } from "app/entries/thunks"
|
||||
import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
|
||||
import { createAppAsyncThunk } from "@/app/async-thunk"
|
||||
import { client } from "@/app/client"
|
||||
import { reloadEntries } from "@/app/entries/thunks"
|
||||
import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "@/app/types"
|
||||
|
||||
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
|
||||
|
||||
@@ -89,6 +89,15 @@ export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||
}
|
||||
)
|
||||
|
||||
export const changeMarkAllAsReadNavigateToUnread = createAppAsyncThunk(
|
||||
"settings/markAllAsReadNavigateToUnread",
|
||||
(markAllAsReadNavigateToNextUnread: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, markAllAsReadNavigateToNextUnread })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { throttle } from "throttle-debounce"
|
||||
import type { TreeCategory } from "@/app/tree/slice"
|
||||
import type { Category } from "./types"
|
||||
|
||||
export function visitCategoryTree(
|
||||
category: Category,
|
||||
visitor: (category: Category) => void,
|
||||
category: TreeCategory,
|
||||
visitor: (category: TreeCategory) => void,
|
||||
options?: {
|
||||
childrenFirst?: boolean
|
||||
}
|
||||
@@ -19,13 +20,13 @@ export function visitCategoryTree(
|
||||
if (childrenFirst) visitor(category)
|
||||
}
|
||||
|
||||
export function flattenCategoryTree(category: Category): Category[] {
|
||||
export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
|
||||
const categories: Category[] = []
|
||||
visitCategoryTree(category, c => categories.push(c))
|
||||
return categories
|
||||
}
|
||||
|
||||
export function categoryUnreadCount(category?: Category): number {
|
||||
export function categoryUnreadCount(category?: TreeCategory): number {
|
||||
if (!category) return 0
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
@@ -34,6 +35,14 @@ export function categoryUnreadCount(category?: Category): number {
|
||||
.reduce((total, current) => total + current, 0)
|
||||
}
|
||||
|
||||
export function categoryHasNewEntries(category?: TreeCategory): boolean {
|
||||
if (!category) return false
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
.flatMap(c => c.feeds)
|
||||
.some(f => f.hasNewEntries)
|
||||
}
|
||||
|
||||
export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => {
|
||||
const placeholderWidth = width && Math.min(width, maxWidth)
|
||||
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { I18nContext } from "@lingui/react"
|
||||
import { MantineProvider } from "@mantine/core"
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { useActionButton } from "@/hooks/useActionButton"
|
||||
import { ActionButton } from "./ActionButton"
|
||||
|
||||
vi.mock(import("@lingui/react"), () => ({
|
||||
@@ -10,7 +10,7 @@ vi.mock(import("@lingui/react"), () => ({
|
||||
_: msg => msg,
|
||||
} as I18nContext),
|
||||
}))
|
||||
vi.mock(import("hooks/useActionButton"))
|
||||
vi.mock(import("@/hooks/useActionButton"))
|
||||
|
||||
const label = "Test Label"
|
||||
const icon = "Test Icon"
|
||||
@@ -18,7 +18,9 @@ describe("ActionButton", () => {
|
||||
it("renders Button with label on desktop", () => {
|
||||
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 })
|
||||
|
||||
render(<ActionButton label={label} icon={icon} />, { wrapper: MantineProvider })
|
||||
render(<ActionButton label={label} icon={icon} />, {
|
||||
wrapper: MantineProvider,
|
||||
})
|
||||
expect(screen.getByText(label)).toBeInTheDocument()
|
||||
expect(screen.getByText(icon)).toBeInTheDocument()
|
||||
})
|
||||
@@ -26,7 +28,9 @@ describe("ActionButton", () => {
|
||||
it("renders ActionIcon with tooltip on mobile", async () => {
|
||||
vi.mocked(useActionButton).mockReturnValue({ mobile: true, spacing: 0 })
|
||||
|
||||
render(<ActionButton label={label} icon={icon} />, { wrapper: MantineProvider })
|
||||
render(<ActionButton label={label} icon={icon} />, {
|
||||
wrapper: MantineProvider,
|
||||
})
|
||||
expect(screen.queryByText(label)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(icon)).toBeInTheDocument()
|
||||
|
||||
@@ -39,7 +43,9 @@ describe("ActionButton", () => {
|
||||
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 })
|
||||
const clickListener = vi.fn()
|
||||
|
||||
render(<ActionButton label={label} icon={icon} onClick={clickListener} />, { wrapper: MantineProvider })
|
||||
render(<ActionButton label={label} icon={icon} onClick={clickListener} />, {
|
||||
wrapper: MantineProvider,
|
||||
})
|
||||
fireEvent.click(screen.getByRole("button"))
|
||||
|
||||
expect(clickListener).toHaveBeenCalled()
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { MessageDescriptor } from "@lingui/core"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { ActionIcon, Box, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
||||
import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
||||
import { Constants } from "app/constants"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { type MouseEventHandler, type ReactNode, forwardRef } from "react"
|
||||
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { useActionButton } from "@/hooks/useActionButton"
|
||||
|
||||
interface ActionButtonProps {
|
||||
icon: ReactNode
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface ErrorsAlertProps {
|
||||
messages: string[]
|
||||
}
|
||||
|
||||
export function Alert(props: ErrorsAlertProps) {
|
||||
export function Alert(props: Readonly<ErrorsAlertProps>) {
|
||||
let title: React.ReactNode
|
||||
let color: string
|
||||
let icon: React.ReactNode
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Dialog, Text } from "@mantine/core"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { setAnnouncementHash } from "app/user/slice"
|
||||
import { Content } from "components/content/Content"
|
||||
import { useAsync } from "react-async-hook"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { setAnnouncementHash } from "@/app/user/slice"
|
||||
import { Content } from "@/components/content/Content"
|
||||
|
||||
const sha256Hex = async (input: string | undefined) => {
|
||||
const data = new TextEncoder().encode(input)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const DisablePullToRefresh = () => {
|
||||
import("./DisablePullToRefresh.css")
|
||||
return <></>
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ErrorPage } from "pages/ErrorPage"
|
||||
import React, { type ReactNode } from "react"
|
||||
import { ErrorPage } from "@/pages/ErrorPage"
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children?: ReactNode
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import { useState } from "react"
|
||||
import { TbPhoto } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
import { tss } from "@/tss"
|
||||
|
||||
interface ImageWithPlaceholderWhileLoadingProps {
|
||||
src: string
|
||||
@@ -44,7 +44,7 @@ export function ImageWithPlaceholderWhileLoading({
|
||||
title,
|
||||
width,
|
||||
style,
|
||||
}: ImageWithPlaceholderWhileLoadingProps) {
|
||||
}: Readonly<ImageWithPlaceholderWhileLoadingProps>) {
|
||||
const { classes } = useStyles({
|
||||
placeholderWidth,
|
||||
placeholderHeight,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
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() {
|
||||
const isMacOS = useOs() === "macos"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Image } from "@mantine/core"
|
||||
import logo from "assets/logo.svg"
|
||||
import logo from "@/assets/logo.svg"
|
||||
|
||||
export interface LogoProps {
|
||||
size: number
|
||||
}
|
||||
|
||||
export function Logo(props: LogoProps) {
|
||||
export function Logo(props: Readonly<LogoProps>) {
|
||||
return <Image src={logo} w={props.size} />
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
||||
import { setMarkAllAsReadConfirmationDialogOpen } from "app/entries/slice"
|
||||
import { markAllEntries } from "app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { useState } from "react"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { setMarkAllAsReadConfirmationDialogOpen } from "@/app/entries/slice"
|
||||
import { markAllEntries } from "@/app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { selectNextUnreadTreeItem } from "@/app/tree/thunks"
|
||||
|
||||
export function MarkAllAsReadConfirmationDialog() {
|
||||
const [threshold, setThreshold] = useState(0)
|
||||
@@ -11,10 +13,12 @@ export function MarkAllAsReadConfirmationDialog() {
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
||||
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
|
||||
const markAllAsReadNavigateToNextUnread = useAppSelector(state => state.user.settings?.markAllAsReadNavigateToNextUnread)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const onConfirm = () => {
|
||||
dispatch(
|
||||
const onConfirm = async () => {
|
||||
dispatch(setMarkAllAsReadConfirmationDialogOpen(false))
|
||||
await dispatch(
|
||||
markAllEntries({
|
||||
sourceType: source.type,
|
||||
req: {
|
||||
@@ -25,7 +29,9 @@ export function MarkAllAsReadConfirmationDialog() {
|
||||
},
|
||||
})
|
||||
)
|
||||
dispatch(setMarkAllAsReadConfirmationDialogOpen(false))
|
||||
|
||||
const isAllCategorySelected = source.type === "category" && source.id === Constants.categories.all.id
|
||||
if (markAllAsReadNavigateToNextUnread && !isAllCategorySelected) await dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import dayjs from "dayjs"
|
||||
import { useNow } from "hooks/useNow"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { useNow } from "@/hooks/useNow"
|
||||
|
||||
export function RelativeDate(props: { date: Date | number | undefined }) {
|
||||
export function RelativeDate(
|
||||
props: Readonly<{
|
||||
date: Date | number | undefined
|
||||
}>
|
||||
) {
|
||||
const now = useNow(60 * 1000)
|
||||
|
||||
if (!props.date) return <Trans>N/A</Trans>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import type { AdminSaveUserRequest, UserModel } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy } from "react-icons/tb"
|
||||
import { client, errorToStrings } from "@/app/client"
|
||||
import type { AdminSaveUserRequest, UserModel } from "@/app/types"
|
||||
import { Alert } from "@/components/Alert"
|
||||
|
||||
interface UserEditProps {
|
||||
user?: UserModel
|
||||
@@ -13,7 +13,7 @@ interface UserEditProps {
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export function UserEdit(props: UserEditProps) {
|
||||
export function UserEdit(props: Readonly<UserEditProps>) {
|
||||
const form = useForm<AdminSaveUserRequest>({
|
||||
initialValues: props.user ?? {
|
||||
name: "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Input, Textarea } from "@mantine/core"
|
||||
import RichCodeEditor from "components/code/RichCodeEditor"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import type { ReactNode } from "react"
|
||||
import RichCodeEditor from "@/components/code/RichCodeEditor"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
|
||||
interface CodeEditorProps {
|
||||
label?: ReactNode
|
||||
@@ -11,7 +11,7 @@ interface CodeEditorProps {
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
export function CodeEditor(props: CodeEditorProps) {
|
||||
export function CodeEditor(props: Readonly<CodeEditorProps>) {
|
||||
const mobile = useMobile()
|
||||
|
||||
return mobile ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Loader } from "components/Loader"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { useAsync } from "react-async-hook"
|
||||
import { Loader } from "@/components/Loader"
|
||||
import { useColorScheme } from "@/hooks/useColorScheme"
|
||||
|
||||
const init = async () => {
|
||||
window.MonacoEnvironment = {
|
||||
@@ -30,7 +30,7 @@ interface RichCodeEditorProps {
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
function RichCodeEditor(props: RichCodeEditorProps) {
|
||||
function RichCodeEditor(props: Readonly<RichCodeEditorProps>) {
|
||||
const colorScheme = useColorScheme()
|
||||
const editorTheme = colorScheme === "dark" ? "vs-dark" : "light"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TypographyStylesProvider } from "@mantine/core"
|
||||
import { Typography } from "@mantine/core"
|
||||
import type { ReactNode } from "react"
|
||||
import { tss } from "tss"
|
||||
import { tss } from "@/tss"
|
||||
|
||||
/**
|
||||
* This component is used to provide basic styles to html typography elements.
|
||||
@@ -20,5 +20,5 @@ const useStyles = tss.create(() => ({
|
||||
|
||||
export const BasicHtmlStyles = (props: { children: ReactNode }) => {
|
||||
const { classes } = useStyles()
|
||||
return <TypographyStylesProvider className={classes.content}>{props.children}</TypographyStylesProvider>
|
||||
return <Typography className={classes.content}>{props.children}</Typography>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MantineProvider } from "@mantine/core"
|
||||
import { render } from "@testing-library/react"
|
||||
import { Content } from "components/content/Content"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { Content } from "@/components/content/Content"
|
||||
|
||||
describe("Content component", () => {
|
||||
it("renders basic content", () => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Box, Mark } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import escapeStringRegexp from "escape-string-regexp"
|
||||
import { ALLOWED_TAG_LIST, type ChildrenNode, Interweave, type MatchResponse, Matcher, type Node, type TransformCallback } from "interweave"
|
||||
import { ALLOWED_TAG_LIST, type ChildrenNode, Interweave, Matcher, type MatchResponse, type Node, type TransformCallback } from "interweave"
|
||||
import React from "react"
|
||||
import styleToObject from "style-to-object"
|
||||
import { tss } from "tss"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { calculatePlaceholderSize } from "@/app/utils"
|
||||
import { BasicHtmlStyles } from "@/components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "@/components/ImageWithPlaceholderWhileLoading"
|
||||
import { tss } from "@/tss"
|
||||
|
||||
export interface ContentProps {
|
||||
content: string
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { BasicHtmlStyles } from "@/components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "@/components/ImageWithPlaceholderWhileLoading"
|
||||
|
||||
export function Enclosure(props: {
|
||||
enclosureType: string
|
||||
enclosureUrl: string
|
||||
}) {
|
||||
export function Enclosure(
|
||||
props: Readonly<{
|
||||
enclosureType: string
|
||||
enclosureUrl: string
|
||||
}>
|
||||
) {
|
||||
const hasVideo = props.enclosureType.startsWith("video")
|
||||
const hasAudio = props.enclosureType.startsWith("audio")
|
||||
const hasImage = props.enclosureType.startsWith("image")
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box } from "@mantine/core"
|
||||
import { openModal } from "@mantine/modals"
|
||||
import { Constants } from "app/constants"
|
||||
import type { ExpendableEntry } from "app/entries/slice"
|
||||
import { useEffect } from "react"
|
||||
import { useContextMenu } from "react-contexify"
|
||||
import InfiniteScroll from "react-infinite-scroller"
|
||||
import { throttle } from "throttle-debounce"
|
||||
import { Constants } from "@/app/constants"
|
||||
import type { ExpendableEntry } from "@/app/entries/slice"
|
||||
import {
|
||||
loadMoreEntries,
|
||||
markAllAsReadWithConfirmationIfRequired,
|
||||
@@ -12,19 +16,15 @@ import {
|
||||
selectNextEntry,
|
||||
selectPreviousEntry,
|
||||
starEntry,
|
||||
} from "app/entries/thunks"
|
||||
import { redirectToRootCategory } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { toggleSidebar } from "app/tree/slice"
|
||||
import { selectNextUnreadTreeItem } from "app/tree/thunks"
|
||||
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { useEffect } from "react"
|
||||
import { useContextMenu } from "react-contexify"
|
||||
import InfiniteScroll from "react-infinite-scroller"
|
||||
import { throttle } from "throttle-debounce"
|
||||
} from "@/app/entries/thunks"
|
||||
import { redirectToRootCategory } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { toggleSidebar } from "@/app/tree/slice"
|
||||
import { selectNextUnreadTreeItem } from "@/app/tree/thunks"
|
||||
import { KeyboardShortcutsHelp } from "@/components/KeyboardShortcutsHelp"
|
||||
import { Loader } from "@/components/Loader"
|
||||
import { useBrowserExtension } from "@/hooks/useBrowserExtension"
|
||||
import { useMousetrap } from "@/hooks/useMousetrap"
|
||||
import { FeedEntry } from "./FeedEntry"
|
||||
|
||||
export function FeedEntries() {
|
||||
@@ -287,7 +287,6 @@ export function FeedEntries() {
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
id="entries"
|
||||
className={`cf-entries cf-view-mode-${viewMode}`}
|
||||
initialLoad={false}
|
||||
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import type { Entry, ViewMode } from "app/types"
|
||||
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
|
||||
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import type React from "react"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
import { tss } from "tss"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { useAppSelector } from "@/app/store"
|
||||
import type { Entry, ViewMode } from "@/app/types"
|
||||
import { FeedEntryCompactHeader } from "@/components/content/header/FeedEntryCompactHeader"
|
||||
import { FeedEntryHeader } from "@/components/content/header/FeedEntryHeader"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
import { tss } from "@/tss"
|
||||
import { FeedEntryBody } from "./FeedEntryBody"
|
||||
import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
|
||||
import { FeedEntryFooter } from "./FeedEntryFooter"
|
||||
@@ -96,7 +96,7 @@ const useStyles = tss
|
||||
}
|
||||
})
|
||||
|
||||
export function FeedEntry(props: FeedEntryProps) {
|
||||
export function FeedEntry(props: Readonly<FeedEntryProps>) {
|
||||
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
|
||||
const fontSizePercentage = useAppSelector(state => state.user.localSettings.fontSizePercentage)
|
||||
const { classes, cx } = useStyles({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { useAppSelector } from "@/app/store"
|
||||
import type { Entry } from "@/app/types"
|
||||
import { Content } from "./Content"
|
||||
import { Enclosure } from "./Enclosure"
|
||||
import { Media } from "./Media"
|
||||
@@ -9,7 +9,7 @@ export interface FeedEntryBodyProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export function FeedEntryBody(props: FeedEntryBodyProps) {
|
||||
export function FeedEntryBody(props: Readonly<FeedEntryBodyProps>) {
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
return (
|
||||
<Box>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Group } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { truncate } from "app/utils"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { Item, Menu, Separator } from "react-contexify"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry } from "@/app/entries/thunks"
|
||||
import { redirectToFeed } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import type { Entry } from "@/app/types"
|
||||
import { truncate } from "@/app/utils"
|
||||
import { useBrowserExtension } from "@/hooks/useBrowserExtension"
|
||||
import { useColorScheme } from "@/hooks/useColorScheme"
|
||||
import { tss } from "@/tss"
|
||||
|
||||
interface FeedEntryContextMenuProps {
|
||||
entry: Entry
|
||||
@@ -27,7 +27,7 @@ const useStyles = tss.create(({ theme, colorScheme }) => ({
|
||||
},
|
||||
}))
|
||||
|
||||
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
export function FeedEntryContextMenu(props: Readonly<FeedEntryContextMenuProps>) {
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles()
|
||||
const sourceType = useAppSelector(state => state.entries.source.type)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "@/app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import type { Entry } from "@/app/types"
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import { useActionButton } from "@/hooks/useActionButton"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
import { ShareButtons } from "./ShareButtons"
|
||||
|
||||
interface FeedEntryFooterProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
export function FeedEntryFooter(props: Readonly<FeedEntryFooterProps>) {
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const mobile = useMobile()
|
||||
const { spacing } = useActionButton()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { ImageWithPlaceholderWhileLoading } from "@/components/ImageWithPlaceholderWhileLoading"
|
||||
|
||||
export interface FeedFaviconProps {
|
||||
url: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function FeedFavicon({ url, size = 18 }: FeedFaviconProps) {
|
||||
export function FeedFavicon({ url, size = 18 }: Readonly<FeedFaviconProps>) {
|
||||
return (
|
||||
<ImageWithPlaceholderWhileLoading
|
||||
src={url}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { calculatePlaceholderSize } from "@/app/utils"
|
||||
import { BasicHtmlStyles } from "@/components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "@/components/ImageWithPlaceholderWhileLoading"
|
||||
import { Content } from "./Content"
|
||||
|
||||
export interface MediaProps {
|
||||
@@ -12,7 +12,7 @@ export interface MediaProps {
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function Media(props: MediaProps) {
|
||||
export function Media(props: Readonly<MediaProps>) {
|
||||
const width = props.thumbnailWidth
|
||||
const height = props.thumbnailHeight
|
||||
const placeholderSize = calculatePlaceholderSize({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import type { SharingSettings } from "app/types"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import type { IconType } from "react-icons"
|
||||
import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { useAppSelector } from "@/app/store"
|
||||
import type { SharingSettings } from "@/app/types"
|
||||
import { useBrowserExtension } from "@/hooks/useBrowserExtension"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
import { tss } from "@/tss"
|
||||
|
||||
type Color = `#${string}`
|
||||
|
||||
@@ -22,7 +22,15 @@ const useStyles = tss
|
||||
},
|
||||
}))
|
||||
|
||||
function ShareButton({ icon, color, onClick }: { icon: IconType; color: Color; onClick: () => void }) {
|
||||
function ShareButton({
|
||||
icon,
|
||||
color,
|
||||
onClick,
|
||||
}: Readonly<{
|
||||
icon: IconType
|
||||
color: Color
|
||||
onClick: () => void
|
||||
}>) {
|
||||
const { classes } = useStyles({
|
||||
color,
|
||||
})
|
||||
@@ -36,7 +44,15 @@ function ShareButton({ icon, color, onClick }: { icon: IconType; color: Color; o
|
||||
)
|
||||
}
|
||||
|
||||
function SiteShareButton({ url, icon, color }: { icon: IconType; color: Color; url: string }) {
|
||||
function SiteShareButton({
|
||||
url,
|
||||
icon,
|
||||
color,
|
||||
}: Readonly<{
|
||||
icon: IconType
|
||||
color: Color
|
||||
url: string
|
||||
}>) {
|
||||
const onClick = () => {
|
||||
window.open(url, "", "menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=800,height=600")
|
||||
}
|
||||
@@ -44,7 +60,11 @@ function SiteShareButton({ url, icon, color }: { icon: IconType; color: Color; u
|
||||
return <ShareButton icon={icon} color={color} onClick={onClick} />
|
||||
}
|
||||
|
||||
function CopyUrlButton({ url }: { url: string }) {
|
||||
function CopyUrlButton({
|
||||
url,
|
||||
}: Readonly<{
|
||||
url: string
|
||||
}>) {
|
||||
return (
|
||||
<CopyButton value={url}>
|
||||
{({ copied, copy }) => <ShareButton icon={copied ? TbCheck : TbCopy} color="#000" onClick={copy} />}
|
||||
@@ -52,7 +72,13 @@ function CopyUrlButton({ url }: { url: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function BrowserNativeShareButton({ url, description }: { url: string; description: string }) {
|
||||
function BrowserNativeShareButton({
|
||||
url,
|
||||
description,
|
||||
}: Readonly<{
|
||||
url: string
|
||||
description: string
|
||||
}>) {
|
||||
const mobile = useMobile()
|
||||
const { isBrowserExtensionPopup } = useBrowserExtension()
|
||||
const onClick = () => {
|
||||
@@ -71,7 +97,12 @@ function BrowserNativeShareButton({ url, description }: { url: string; descripti
|
||||
)
|
||||
}
|
||||
|
||||
export function ShareButtons(props: { url: string; description: string }) {
|
||||
export function ShareButtons(
|
||||
props: Readonly<{
|
||||
url: string
|
||||
description: string
|
||||
}>
|
||||
) {
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const enabledSharingSites = (Object.keys(Constants.sharing) as Array<keyof SharingSettings>).filter(site => sharingSettings?.[site])
|
||||
const url = encodeURIComponent(props.url)
|
||||
|
||||
@@ -3,14 +3,14 @@ import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import type { AddCategoryRequest } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbFolderPlus } from "react-icons/tb"
|
||||
import { client, errorToStrings } from "@/app/client"
|
||||
import { redirectToSelectedSource } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch } from "@/app/store"
|
||||
import { reloadTree } from "@/app/tree/thunks"
|
||||
import type { AddCategoryRequest } from "@/app/types"
|
||||
import { Alert } from "@/components/Alert"
|
||||
import { CategorySelect } from "./CategorySelect"
|
||||
|
||||
export function AddCategory() {
|
||||
|
||||
@@ -2,10 +2,10 @@ import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Select, type SelectProps } from "@mantine/core"
|
||||
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import type { Category } from "app/types"
|
||||
import { flattenCategoryTree } from "app/utils"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { useAppSelector } from "@/app/store"
|
||||
import type { Category } from "@/app/types"
|
||||
import { flattenCategoryTree } from "@/app/utils"
|
||||
|
||||
type CategorySelectProps = Partial<SelectProps> & {
|
||||
withAll?: boolean
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||
import { isNotEmpty, useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbFileImport } from "react-icons/tb"
|
||||
import { client, errorToStrings } from "@/app/client"
|
||||
import { redirectToSelectedSource } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch } from "@/app/store"
|
||||
import { reloadTree } from "@/app/tree/thunks"
|
||||
import { Alert } from "@/components/Alert"
|
||||
|
||||
export function ImportOpml() {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import type { FeedInfoRequest, SubscribeRequest } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useState } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbRss } from "react-icons/tb"
|
||||
import { client, errorToStrings } from "@/app/client"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { redirectToFeed, redirectToSelectedSource } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch } from "@/app/store"
|
||||
import { reloadTree } from "@/app/tree/thunks"
|
||||
import type { FeedInfoRequest, SubscribeRequest } from "@/app/types"
|
||||
import { Alert } from "@/components/Alert"
|
||||
import { CategorySelect } from "./CategorySelect"
|
||||
|
||||
export function Subscribe() {
|
||||
@@ -39,9 +39,8 @@ export function Subscribe() {
|
||||
},
|
||||
})
|
||||
const subscribe = useAsyncCallback(client.feed.subscribe, {
|
||||
onSuccess: async sub => {
|
||||
await dispatch(reloadTree())
|
||||
dispatch(redirectToFeed(sub.data))
|
||||
onSuccess: sub => {
|
||||
dispatch(reloadTree()).then(() => dispatch(redirectToFeed(sub.data)))
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import type { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||
import { Star } from "components/content/header/Star"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { tss } from "tss"
|
||||
import type { Entry } from "@/app/types"
|
||||
import { FeedFavicon } from "@/components/content/FeedFavicon"
|
||||
import { OpenExternalLink } from "@/components/content/header/OpenExternalLink"
|
||||
import { Star } from "@/components/content/header/Star"
|
||||
import { RelativeDate } from "@/components/RelativeDate"
|
||||
import { OnDesktop } from "@/components/responsive/OnDesktop"
|
||||
import { tss } from "@/tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
@@ -43,7 +43,7 @@ const useStyles = tss
|
||||
},
|
||||
}))
|
||||
|
||||
export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
||||
export function FeedEntryCompactHeader(props: Readonly<FeedEntryHeaderProps>) {
|
||||
const { classes } = useStyles({
|
||||
read: props.entry.read,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Box, Flex, Space } from "@mantine/core"
|
||||
import type { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||
import { Star } from "components/content/header/Star"
|
||||
import { tss } from "tss"
|
||||
import type { Entry } from "@/app/types"
|
||||
import { FeedFavicon } from "@/components/content/FeedFavicon"
|
||||
import { OpenExternalLink } from "@/components/content/header/OpenExternalLink"
|
||||
import { Star } from "@/components/content/header/Star"
|
||||
import { RelativeDate } from "@/components/RelativeDate"
|
||||
import { tss } from "@/tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
@@ -24,7 +24,7 @@ const useStyles = tss
|
||||
},
|
||||
}))
|
||||
|
||||
export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
export function FeedEntryHeader(props: Readonly<FeedEntryHeaderProps>) {
|
||||
const { classes } = useStyles({
|
||||
read: props.entry.read,
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Highlight } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { useAppSelector } from "@/app/store"
|
||||
import type { Entry } from "@/app/types"
|
||||
|
||||
export interface FeedEntryTitleProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export function FeedEntryTitle(props: FeedEntryTitleProps) {
|
||||
export function FeedEntryTitle(props: Readonly<FeedEntryTitleProps>) {
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
const keywords = search?.split(" ")
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { ActionIcon, Anchor, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { TbExternalLink } from "react-icons/tb"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { markEntry } from "@/app/entries/thunks"
|
||||
import { useAppDispatch } from "@/app/store"
|
||||
import type { Entry } from "@/app/types"
|
||||
|
||||
export function OpenExternalLink(props: { entry: Entry }) {
|
||||
export function OpenExternalLink(
|
||||
props: Readonly<{
|
||||
entry: Entry
|
||||
}>
|
||||
) {
|
||||
const dispatch = useAppDispatch()
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { ActionIcon, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { starEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { TbStar, TbStarFilled } from "react-icons/tb"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { starEntry } from "@/app/entries/thunks"
|
||||
import { useAppDispatch } from "@/app/store"
|
||||
import type { Entry } from "@/app/types"
|
||||
|
||||
export function Star(props: { entry: Entry }) {
|
||||
export function Star(
|
||||
props: Readonly<{
|
||||
entry: Entry
|
||||
}>
|
||||
) {
|
||||
const dispatch = useAppDispatch()
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -2,14 +2,6 @@ import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { markAllAsReadWithConfirmationIfRequired, reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { changeReadingMode, changeReadingOrder } from "app/user/thunks"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useEffect } from "react"
|
||||
import {
|
||||
TbArrowDown,
|
||||
@@ -25,6 +17,14 @@ import {
|
||||
TbSortDescending,
|
||||
TbUser,
|
||||
} from "react-icons/tb"
|
||||
import { markAllAsReadWithConfirmationIfRequired, reloadEntries, search, selectNextEntry, selectPreviousEntry } from "@/app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { changeReadingMode, changeReadingOrder } from "@/app/user/thunks"
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import { Loader } from "@/components/Loader"
|
||||
import { useActionButton } from "@/hooks/useActionButton"
|
||||
import { useBrowserExtension } from "@/hooks/useBrowserExtension"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
import { ProfileMenu } from "./ProfileMenu"
|
||||
|
||||
function HeaderDivider() {
|
||||
|
||||
@@ -11,14 +11,7 @@ import {
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core"
|
||||
import { showNotification } from "@mantine/notifications"
|
||||
import { client } from "app/client"
|
||||
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { ViewMode } from "app/types"
|
||||
import { setFontSizePercentage, setViewMode } from "app/user/slice"
|
||||
import { reloadProfile } from "app/user/thunks"
|
||||
import dayjs from "dayjs"
|
||||
import { useNow } from "hooks/useNow"
|
||||
import { type ReactNode, useState } from "react"
|
||||
import {
|
||||
TbChartLine,
|
||||
@@ -36,6 +29,13 @@ import {
|
||||
TbUsers,
|
||||
TbWorldDownload,
|
||||
} from "react-icons/tb"
|
||||
import { client } from "@/app/client"
|
||||
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import type { ViewMode } from "@/app/types"
|
||||
import { setFontSizePercentage, setViewMode } from "@/app/user/slice"
|
||||
import { reloadProfile } from "@/app/user/thunks"
|
||||
import { useNow } from "@/hooks/useNow"
|
||||
|
||||
interface ProfileMenuProps {
|
||||
control: React.ReactElement
|
||||
@@ -94,7 +94,7 @@ const viewModeData: ViewModeControlItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export function ProfileMenu(props: ProfileMenuProps) {
|
||||
export function ProfileMenu(props: Readonly<ProfileMenuProps>) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const now = useNow()
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
@@ -145,7 +145,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
||||
color: "green",
|
||||
autoClose: 1000,
|
||||
})
|
||||
} catch (_) {
|
||||
} catch {
|
||||
showNotification({
|
||||
message: <Trans>Force fetching feeds is not yet available.</Trans>,
|
||||
color: "red",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NumberFormatter } from "@mantine/core"
|
||||
import type { MetricGauge } from "app/types"
|
||||
import type { MetricGauge } from "@/app/types"
|
||||
|
||||
interface MeterProps {
|
||||
interface GaugeProps {
|
||||
gauge: MetricGauge
|
||||
}
|
||||
|
||||
export function Gauge(props: MeterProps) {
|
||||
export function Gauge(props: Readonly<GaugeProps>) {
|
||||
return <NumberFormatter value={props.gauge.value} thousandSeparator />
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import type { MetricMeter } from "app/types"
|
||||
import type { MetricMeter } from "@/app/types"
|
||||
|
||||
interface MeterProps {
|
||||
meter: MetricMeter
|
||||
}
|
||||
|
||||
export function Meter(props: MeterProps) {
|
||||
export function Meter(props: Readonly<MeterProps>) {
|
||||
return (
|
||||
<Box>
|
||||
<Box>Mean: {props.meter.mean_rate.toFixed(2)}</Box>
|
||||
|
||||
@@ -7,7 +7,7 @@ interface MetricAccordionItemProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function MetricAccordionItem({ metricKey, name, headerValue, children }: MetricAccordionItemProps) {
|
||||
export function MetricAccordionItem({ metricKey, name, headerValue, children }: Readonly<MetricAccordionItemProps>) {
|
||||
return (
|
||||
<Accordion.Item value={metricKey} key={metricKey}>
|
||||
<Accordion.Control>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import type { MetricTimer } from "app/types"
|
||||
import type { MetricTimer } from "@/app/types"
|
||||
|
||||
interface MetricTimerProps {
|
||||
timer: MetricTimer
|
||||
}
|
||||
|
||||
export function Timer(props: MetricTimerProps) {
|
||||
export function Timer(props: Readonly<MetricTimerProps>) {
|
||||
return (
|
||||
<Box>
|
||||
<Box>Mean: {props.timer.mean_rate.toFixed(2)}</Box>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import type React from "react"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
|
||||
export function OnDesktop(props: { children: React.ReactNode }) {
|
||||
export function OnDesktop(
|
||||
props: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>
|
||||
) {
|
||||
const mobile = useMobile()
|
||||
return <Box>{!mobile && props.children}</Box>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import type React from "react"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
|
||||
export function OnMobile(props: { children: React.ReactNode }) {
|
||||
export function OnMobile(
|
||||
props: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>
|
||||
) {
|
||||
const mobile = useMobile()
|
||||
return <Box>{mobile && props.children}</Box>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Button, Group, Stack } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Alert } from "components/Alert"
|
||||
import { CodeEditor } from "components/code/CodeEditor"
|
||||
import { useEffect } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy } from "react-icons/tb"
|
||||
import { client, errorToStrings } from "@/app/client"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { redirectToSelectedSource } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { Alert } from "@/components/Alert"
|
||||
import { CodeEditor } from "@/components/code/CodeEditor"
|
||||
|
||||
interface FormData {
|
||||
customCss: string
|
||||
@@ -60,18 +60,14 @@ export function CustomCodeSettings() {
|
||||
<CodeEditor
|
||||
label={<Trans>Custom CSS rules that will be applied</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
<span>See </span>
|
||||
<Anchor
|
||||
href={Constants.customCssDocumentationUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ fontSize: "inherit" }}
|
||||
>
|
||||
here
|
||||
</Anchor>
|
||||
<span> for more information.</span>
|
||||
</Trans>
|
||||
<Anchor
|
||||
href={Constants.customCssDocumentationUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ fontSize: "inherit" }}
|
||||
>
|
||||
<Trans>Link to the documentation</Trans>
|
||||
</Anchor>
|
||||
}
|
||||
language="css"
|
||||
{...form.getInputProps("customCss")}
|
||||
|
||||
@@ -3,15 +3,17 @@ import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Divider, Group, NumberInput, Radio, Select, type SelectProps, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { IconDisplayMode, ScrollMode, SharingSettings } from "app/types"
|
||||
import type { ReactNode } from "react"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import type { IconDisplayMode, ScrollMode, SharingSettings } from "@/app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMarkAllAsReadNavigateToUnread,
|
||||
changeMobileFooter,
|
||||
changePrimaryColor,
|
||||
changeScrollMarks,
|
||||
@@ -22,9 +24,8 @@ import {
|
||||
changeStarIconDisplayMode,
|
||||
changeUnreadCountFavicon,
|
||||
changeUnreadCountTitle,
|
||||
} from "app/user/thunks"
|
||||
import { locales } from "i18n"
|
||||
import type { ReactNode } from "react"
|
||||
} from "@/app/user/thunks"
|
||||
import { locales } from "@/i18n"
|
||||
|
||||
export function DisplaySettings() {
|
||||
const language = useAppSelector(state => state.user.settings?.language)
|
||||
@@ -36,6 +37,7 @@ export function DisplaySettings() {
|
||||
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
|
||||
const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
|
||||
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
|
||||
const markAllAsReadNavigateToNextUnread = useAppSelector(state => state.user.settings?.markAllAsReadNavigateToNextUnread)
|
||||
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
@@ -127,6 +129,12 @@ export function DisplaySettings() {
|
||||
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Navigate to the next category/feed with unread entries when marking all entries as read</Trans>}
|
||||
checked={markAllAsReadNavigateToNextUnread}
|
||||
onChange={async e => await dispatch(changeMarkAllAsReadNavigateToUnread(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
|
||||
checked={mobileFooter}
|
||||
|
||||
@@ -4,15 +4,15 @@ import { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { openConfirmModal } from "@mantine/modals"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { ProfileModificationRequest } from "app/types"
|
||||
import { reloadProfile } from "app/user/thunks"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useEffect } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
||||
import { client, errorToStrings } from "@/app/client"
|
||||
import { redirectToLogin, redirectToSelectedSource } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import type { ProfileModificationRequest } from "@/app/types"
|
||||
import { reloadProfile } from "@/app/user/thunks"
|
||||
import { Alert } from "@/components/Alert"
|
||||
|
||||
interface FormData extends ProfileModificationRequest {
|
||||
newPasswordConfirmation?: string
|
||||
@@ -52,7 +52,9 @@ export function ProfileSettings() {
|
||||
),
|
||||
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: async () => await deleteProfile.execute(),
|
||||
onConfirm: () => {
|
||||
deleteProfile.execute()
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Stack } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import React from "react"
|
||||
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||
import { Constants } from "@/app/constants"
|
||||
import {
|
||||
redirectToCategory,
|
||||
redirectToCategoryDetails,
|
||||
@@ -8,15 +10,14 @@ import {
|
||||
redirectToFeedDetails,
|
||||
redirectToTag,
|
||||
redirectToTagDetails,
|
||||
} from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { collapseTreeCategory } from "app/tree/thunks"
|
||||
import type { Category, Subscription } from "app/types"
|
||||
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
||||
import { Loader } from "components/Loader"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import React from "react"
|
||||
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||
} from "@/app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import type { TreeSubscription } from "@/app/tree/slice"
|
||||
import { collapseTreeCategory } from "@/app/tree/thunks"
|
||||
import type { Category, Subscription } from "@/app/types"
|
||||
import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "@/app/utils"
|
||||
import { Loader } from "@/components/Loader"
|
||||
import { OnDesktop } from "@/components/responsive/OnDesktop"
|
||||
import { TreeNode } from "./TreeNode"
|
||||
import { TreeSearch } from "./TreeSearch"
|
||||
|
||||
@@ -89,6 +90,7 @@ export function Tree() {
|
||||
name={<Trans>All</Trans>}
|
||||
icon={allIcon}
|
||||
unread={categoryUnreadCount(root)}
|
||||
hasNewEntries={categoryHasNewEntries(root)}
|
||||
selected={source.type === "category" && source.id === Constants.categories.all.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
@@ -103,6 +105,7 @@ export function Tree() {
|
||||
name={<Trans>Starred</Trans>}
|
||||
icon={starredIcon}
|
||||
unread={0}
|
||||
hasNewEntries={false}
|
||||
selected={source.type === "category" && source.id === Constants.categories.starred.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
@@ -122,6 +125,7 @@ export function Tree() {
|
||||
name={category.name}
|
||||
icon={category.expanded ? expandedIcon : collapsedIcon}
|
||||
unread={categoryUnreadCount(category)}
|
||||
hasNewEntries={categoryHasNewEntries(category)}
|
||||
selected={source.type === "category" && source.id === category.id}
|
||||
expanded={category.expanded}
|
||||
level={level}
|
||||
@@ -133,7 +137,7 @@ export function Tree() {
|
||||
)
|
||||
}
|
||||
|
||||
const feedNode = (feed: Subscription, level = 0) => {
|
||||
const feedNode = (feed: TreeSubscription, level = 0) => {
|
||||
if (!isFeedDisplayed(feed)) return null
|
||||
|
||||
return (
|
||||
@@ -143,6 +147,7 @@ export function Tree() {
|
||||
name={feed.name}
|
||||
icon={feed.iconUrl}
|
||||
unread={feed.unread}
|
||||
hasNewEntries={!!feed.hasNewEntries}
|
||||
selected={source.type === "feed" && source.id === String(feed.id)}
|
||||
level={level}
|
||||
hasError={feed.errorCount > errorThreshold}
|
||||
@@ -159,6 +164,7 @@ export function Tree() {
|
||||
name={tag}
|
||||
icon={tagIcon}
|
||||
unread={0}
|
||||
hasNewEntries={false}
|
||||
selected={source.type === "tag" && source.id === tag}
|
||||
level={0}
|
||||
hasError={false}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import type { EntrySourceType } from "app/entries/slice"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import type React from "react"
|
||||
import { tss } from "tss"
|
||||
import type { EntrySourceType } from "@/app/entries/slice"
|
||||
import { FeedFavicon } from "@/components/content/FeedFavicon"
|
||||
import { tss } from "@/tss"
|
||||
import { UnreadCount } from "./UnreadCount"
|
||||
|
||||
interface TreeNodeProps {
|
||||
@@ -15,6 +15,7 @@ interface TreeNodeProps {
|
||||
expanded?: boolean
|
||||
level: number
|
||||
hasError: boolean
|
||||
hasNewEntries: boolean
|
||||
onClick: (e: React.MouseEvent, id: string) => void
|
||||
onIconClick?: (e: React.MouseEvent, id: string) => void
|
||||
}
|
||||
@@ -58,7 +59,7 @@ const useStyles = tss
|
||||
}
|
||||
})
|
||||
|
||||
export function TreeNode(props: TreeNodeProps) {
|
||||
export function TreeNode(props: Readonly<TreeNodeProps>) {
|
||||
const { classes } = useStyles({
|
||||
selected: props.selected,
|
||||
hasError: props.hasError,
|
||||
@@ -80,7 +81,7 @@ export function TreeNode(props: TreeNodeProps) {
|
||||
<Box className={classes.nodeText}>{props.name}</Box>
|
||||
{!props.expanded && (
|
||||
<Box className="cf-treenode-unread-count">
|
||||
<UnreadCount unreadCount={props.unread} />
|
||||
<UnreadCount unreadCount={props.unread} showIndicator={props.hasNewEntries} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -3,18 +3,18 @@ import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, TextInput } from "@mantine/core"
|
||||
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import type { Subscription } from "app/types"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { TbSearch } from "react-icons/tb"
|
||||
import { redirectToFeed } from "@/app/redirect/thunks"
|
||||
import { useAppDispatch } from "@/app/store"
|
||||
import type { Subscription } from "@/app/types"
|
||||
import { FeedFavicon } from "@/components/content/FeedFavicon"
|
||||
import { useMousetrap } from "@/hooks/useMousetrap"
|
||||
|
||||
export interface TreeSearchProps {
|
||||
feeds: Subscription[]
|
||||
}
|
||||
|
||||
export function TreeSearch(props: TreeSearchProps) {
|
||||
export function TreeSearch(props: Readonly<TreeSearchProps>) {
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Badge, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { tss } from "tss"
|
||||
import { Badge, Indicator, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "@/app/constants"
|
||||
import { tss } from "@/tss"
|
||||
|
||||
const useStyles = tss.create(() => ({
|
||||
badge: {
|
||||
@@ -10,7 +10,12 @@ const useStyles = tss.create(() => ({
|
||||
},
|
||||
}))
|
||||
|
||||
export function UnreadCount(props: { unreadCount: number }) {
|
||||
export function UnreadCount(
|
||||
props: Readonly<{
|
||||
unreadCount: number
|
||||
showIndicator: boolean
|
||||
}>
|
||||
) {
|
||||
const { classes } = useStyles()
|
||||
|
||||
if (props.unreadCount <= 0) return null
|
||||
@@ -18,9 +23,11 @@ export function UnreadCount(props: { unreadCount: number }) {
|
||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||
return (
|
||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
|
||||
{count}
|
||||
</Badge>
|
||||
<Indicator disabled={!props.showIndicator} size={4} offset={10} position="middle-start">
|
||||
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
|
||||
{count}
|
||||
</Badge>
|
||||
</Indicator>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMantineTheme } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useMobile } from "@/hooks/useMobile"
|
||||
|
||||
export const useActionButton = () => {
|
||||
const theme = useMantineTheme()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { useAppSelector } from "@/app/store"
|
||||
|
||||
interface Step {
|
||||
label: string
|
||||
@@ -8,28 +8,28 @@ interface Step {
|
||||
}
|
||||
|
||||
export const useAppLoading = () => {
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const profileLoaded = useAppSelector(state => !!state.user.profile)
|
||||
const settingsLoaded = useAppSelector(state => !!state.user.settings)
|
||||
const rootCategoryLoaded = useAppSelector(state => !!state.tree.rootCategory)
|
||||
const tagsLoaded = useAppSelector(state => !!state.user.tags)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: _(msg`Loading settings...`),
|
||||
done: !!settings,
|
||||
done: settingsLoaded,
|
||||
},
|
||||
{
|
||||
label: _(msg`Loading profile...`),
|
||||
done: !!profile,
|
||||
done: profileLoaded,
|
||||
},
|
||||
{
|
||||
label: _(msg`Loading subscriptions...`),
|
||||
done: !!rootCategory,
|
||||
done: rootCategoryLoaded,
|
||||
},
|
||||
{
|
||||
label: _(msg`Loading tags...`),
|
||||
done: !!tags,
|
||||
done: tagsLoaded,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
import { Constants } from "app/constants"
|
||||
import { Constants } from "@/app/constants"
|
||||
|
||||
export const useMobile = (breakpoint: string | number = Constants.layout.mobileBreakpoint) => {
|
||||
const bp = typeof breakpoint === "number" ? `${breakpoint}px` : breakpoint
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { setWebSocketConnected } from "app/server/slice"
|
||||
import { type AppDispatch, useAppDispatch, useAppSelector } from "app/store"
|
||||
import { newFeedEntriesDiscovered } from "app/tree/thunks"
|
||||
import { useEffect } from "react"
|
||||
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
|
||||
import { setWebSocketConnected } from "@/app/server/slice"
|
||||
import { type AppDispatch, useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { newFeedEntriesDiscovered } from "@/app/tree/thunks"
|
||||
|
||||
const handleMessage = (dispatch: AppDispatch, message: string) => {
|
||||
const parts = message.split(":")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Messages, i18n } from "@lingui/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { i18n, type Messages } from "@lingui/core"
|
||||
import dayjs from "dayjs"
|
||||
import { useEffect } from "react"
|
||||
import { useAppSelector } from "@/app/store"
|
||||
|
||||
interface Locale {
|
||||
key: string
|
||||
@@ -12,34 +12,146 @@ interface Locale {
|
||||
// add an object to the array to add a new locale
|
||||
// don't forget to also add it to the 'locales' array in lingui.config.ts
|
||||
export const locales: Locale[] = [
|
||||
{ key: "ar", label: "العربية", dayjsImportFn: async () => await import("dayjs/locale/ar") },
|
||||
{ key: "ca", label: "Català", dayjsImportFn: async () => await import("dayjs/locale/ca") },
|
||||
{ key: "cs", label: "Čeština", dayjsImportFn: async () => await import("dayjs/locale/cs") },
|
||||
{ key: "cy", label: "Cymraeg", dayjsImportFn: async () => await import("dayjs/locale/cy") },
|
||||
{ key: "da", label: "Danish", dayjsImportFn: async () => await import("dayjs/locale/da") },
|
||||
{ key: "de", label: "Deutsch", dayjsImportFn: async () => await import("dayjs/locale/de") },
|
||||
{ key: "en", label: "English", dayjsImportFn: async () => await import("dayjs/locale/en") },
|
||||
{ key: "es", label: "Español", dayjsImportFn: async () => await import("dayjs/locale/es") },
|
||||
{ key: "fa", label: "فارسی", dayjsImportFn: async () => await import("dayjs/locale/fa") },
|
||||
{ key: "fi", label: "Suomi", dayjsImportFn: async () => await import("dayjs/locale/fi") },
|
||||
{ key: "fr", label: "Français", dayjsImportFn: async () => await import("dayjs/locale/fr") },
|
||||
{ key: "gl", label: "Galician", dayjsImportFn: async () => await import("dayjs/locale/gl") },
|
||||
{ key: "hu", label: "Magyar", dayjsImportFn: async () => await import("dayjs/locale/hu") },
|
||||
{ key: "id", label: "Indonesian", dayjsImportFn: async () => await import("dayjs/locale/id") },
|
||||
{ key: "it", label: "Italiano", dayjsImportFn: async () => await import("dayjs/locale/it") },
|
||||
{ key: "ja", label: "日本語", dayjsImportFn: async () => await import("dayjs/locale/ja") },
|
||||
{ key: "ko", label: "한국어", dayjsImportFn: async () => await import("dayjs/locale/ko") },
|
||||
{ key: "ms", label: "Bahasa Malaysian", dayjsImportFn: async () => await import("dayjs/locale/ms") },
|
||||
{ key: "nb", label: "Norsk (bokmål)", dayjsImportFn: async () => await import("dayjs/locale/nb") },
|
||||
{ key: "nl", label: "Nederlands", dayjsImportFn: async () => await import("dayjs/locale/nl") },
|
||||
{ key: "nn", label: "Norsk (nynorsk)", dayjsImportFn: async () => await import("dayjs/locale/nn") },
|
||||
{ key: "pl", label: "Polski", dayjsImportFn: async () => await import("dayjs/locale/pl") },
|
||||
{ key: "pt", label: "Português", dayjsImportFn: async () => await import("dayjs/locale/pt") },
|
||||
{ key: "ru", label: "Русский", dayjsImportFn: async () => await import("dayjs/locale/ru") },
|
||||
{ key: "sk", label: "Slovenčina", dayjsImportFn: async () => await import("dayjs/locale/sk") },
|
||||
{ key: "sv", label: "Svenska", dayjsImportFn: async () => await import("dayjs/locale/sv") },
|
||||
{ key: "tr", label: "Türkçe", dayjsImportFn: async () => await import("dayjs/locale/tr") },
|
||||
{ key: "zh", label: "简体中文", dayjsImportFn: async () => await import("dayjs/locale/zh") },
|
||||
{
|
||||
key: "ar",
|
||||
label: "العربية",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/ar"),
|
||||
},
|
||||
{
|
||||
key: "ca",
|
||||
label: "Català",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/ca"),
|
||||
},
|
||||
{
|
||||
key: "cs",
|
||||
label: "Čeština",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/cs"),
|
||||
},
|
||||
{
|
||||
key: "cy",
|
||||
label: "Cymraeg",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/cy"),
|
||||
},
|
||||
{
|
||||
key: "da",
|
||||
label: "Danish",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/da"),
|
||||
},
|
||||
{
|
||||
key: "de",
|
||||
label: "Deutsch",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/de"),
|
||||
},
|
||||
{
|
||||
key: "en",
|
||||
label: "English",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/en"),
|
||||
},
|
||||
{
|
||||
key: "es",
|
||||
label: "Español",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/es"),
|
||||
},
|
||||
{
|
||||
key: "fa",
|
||||
label: "فارسی",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/fa"),
|
||||
},
|
||||
{
|
||||
key: "fi",
|
||||
label: "Suomi",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/fi"),
|
||||
},
|
||||
{
|
||||
key: "fr",
|
||||
label: "Français",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/fr"),
|
||||
},
|
||||
{
|
||||
key: "gl",
|
||||
label: "Galician",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/gl"),
|
||||
},
|
||||
{
|
||||
key: "hu",
|
||||
label: "Magyar",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/hu"),
|
||||
},
|
||||
{
|
||||
key: "id",
|
||||
label: "Indonesian",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/id"),
|
||||
},
|
||||
{
|
||||
key: "it",
|
||||
label: "Italiano",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/it"),
|
||||
},
|
||||
{
|
||||
key: "ja",
|
||||
label: "日本語",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/ja"),
|
||||
},
|
||||
{
|
||||
key: "ko",
|
||||
label: "한국어",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/ko"),
|
||||
},
|
||||
{
|
||||
key: "ms",
|
||||
label: "Bahasa Malaysian",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/ms"),
|
||||
},
|
||||
{
|
||||
key: "nb",
|
||||
label: "Norsk (bokmål)",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/nb"),
|
||||
},
|
||||
{
|
||||
key: "nl",
|
||||
label: "Nederlands",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/nl"),
|
||||
},
|
||||
{
|
||||
key: "nn",
|
||||
label: "Norsk (nynorsk)",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/nn"),
|
||||
},
|
||||
{
|
||||
key: "pl",
|
||||
label: "Polski",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/pl"),
|
||||
},
|
||||
{
|
||||
key: "pt",
|
||||
label: "Português",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/pt"),
|
||||
},
|
||||
{
|
||||
key: "ru",
|
||||
label: "Русский",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/ru"),
|
||||
},
|
||||
{
|
||||
key: "sk",
|
||||
label: "Slovenčina",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/sk"),
|
||||
},
|
||||
{
|
||||
key: "sv",
|
||||
label: "Svenska",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/sv"),
|
||||
},
|
||||
{
|
||||
key: "tr",
|
||||
label: "Türkçe",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/tr"),
|
||||
},
|
||||
{
|
||||
key: "zh",
|
||||
label: "简体中文",
|
||||
dayjsImportFn: async () => await import("dayjs/locale/zh"),
|
||||
},
|
||||
]
|
||||
|
||||
function activateLocale(locale: string) {
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0> هل تحتاج إلى حساب؟ </0> <1> اشترك! </ 1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "رابط"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "تحميل ملف التعريف ..."
|
||||
@@ -602,6 +602,10 @@ msgstr "الاسم"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "انتقل إلى اشتراك بإدخال اسمه"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,8 +18,8 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr "<0>CommaFeed és un projecte de codi obert. El codi font està allotjat a </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr "<0>La sintaxi completa està disponible </0><1>aquí</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>La sintaxi completa està disponible </0><1>aquí</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -33,10 +33,6 @@ msgstr "<0>Ei,</0><1> sóc la Jérémie de Bèlgica i fa més de 10 anys que tre
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Necessites un compte?</0><1>Registreu-vos!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -139,7 +135,7 @@ msgstr "Tornar a iniciar sessió"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Blue"
|
||||
msgstr ""
|
||||
msgstr "Blau"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
@@ -151,7 +147,7 @@ msgstr "Extensió del navegador"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
msgstr "Pestanya del navegador"
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
@@ -191,7 +187,7 @@ msgstr "Tanca el menu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
msgstr "Cmd"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
@@ -251,7 +247,7 @@ msgstr "Codi JS personalitzat que s'executarà en carregar la pàgina"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Cyan"
|
||||
msgstr ""
|
||||
msgstr "Cian"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
@@ -338,11 +334,11 @@ msgstr "introduïu la vostra contrasenya actual per canviar la configuració del
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
msgstr "Entrades que es mantindran a sobre de l'entrada seleccionada en desplaçar-se"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
msgstr "Encapçalaments d'entrada"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
@@ -381,11 +377,11 @@ msgstr "Carrega tots els meus feeds ara"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API"
|
||||
msgstr ""
|
||||
msgstr "Fever API"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
msgstr "URL de Fever API"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
@@ -393,11 +389,11 @@ msgstr "Expressió de filtratge"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Font size"
|
||||
msgstr ""
|
||||
msgstr "Mida de la lletra"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
msgstr "La recuperació forçada de feeds encara no està disponible."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
@@ -438,15 +434,15 @@ msgstr "Bones"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Grape"
|
||||
msgstr ""
|
||||
msgstr "Raïm"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Gray"
|
||||
msgstr ""
|
||||
msgstr "Gris"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Green"
|
||||
msgstr ""
|
||||
msgstr "Verd"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
@@ -470,11 +466,11 @@ msgstr "Importació"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "a la vista ampliada, desplaçant-se per les entrades les marqueu com a llegides"
|
||||
msgstr "En la vista ampliada, en desplaçar-se per les entrades, es marquen com a llegides"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Indigo"
|
||||
msgstr ""
|
||||
msgstr "Indi"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
@@ -508,7 +504,7 @@ msgstr "Clar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Lime"
|
||||
msgstr ""
|
||||
msgstr "Llima"
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Enllaç"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr "Enllaç a la documentació"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Carregant el perfil..."
|
||||
@@ -544,7 +544,7 @@ msgstr "Tanca sessió"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
msgstr "Prem llargament la tecla"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
@@ -589,7 +589,7 @@ msgstr "Mou la pàgina cap amunt"
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
msgstr "No es coneix"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
@@ -602,6 +602,10 @@ msgstr "Nom"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegueu a una subscripció introduint-ne el nom"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr "Navega a la següent categoria/canal amb entrades no llegides quan es marquen totes les entrades com a llegides"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
@@ -634,7 +638,7 @@ msgstr "No hi ha més entrades"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
msgstr "No hi ha opcions de compartició disponibles."
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
@@ -646,11 +650,11 @@ msgstr "el més vell primer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
msgstr "A l'scriptori"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
msgstr "Al mòbil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
@@ -658,7 +662,7 @@ msgstr "Al mòbil, mostra els botons d'acció a la part inferior de la pantalla"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
msgstr "Només s'aplica als modes compacte, acollidor i detallat"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
@@ -720,11 +724,11 @@ msgstr "Fitxer OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
msgstr "Cal un fitxer OPML"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Orange"
|
||||
msgstr ""
|
||||
msgstr "Taronja"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
@@ -756,7 +760,7 @@ msgstr "Les contrasenyes no coincideixen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Pink"
|
||||
msgstr ""
|
||||
msgstr "Rosa"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
@@ -769,7 +773,7 @@ msgstr "Anterior"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Primary color"
|
||||
msgstr ""
|
||||
msgstr "Color primari"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
@@ -781,7 +785,7 @@ msgstr "Recuperar la contrasenya"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
msgstr "Vermell"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
@@ -830,19 +834,19 @@ msgstr "Cerca"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "la cerca requereix almenys 3 caràcters"
|
||||
msgstr "La cerca requereix almenys 3 caràcters"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr ""
|
||||
msgstr "Selecciona el següent canal/categoria no llegit"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr ""
|
||||
msgstr "Selecciona el canal/categoria anterior sense llegir"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "posa el focus a la següent entrada sense obrir-la"
|
||||
msgstr "Posa el focus a la següent entrada sense obrir-la"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
@@ -862,7 +866,7 @@ msgstr "Comparteix"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Compartir llocs"
|
||||
msgstr "Compartir a altres llocs web"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
@@ -888,7 +892,7 @@ msgstr "Mostra el menú d'entrada (mòbil)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
msgstr "Mostra la icona d'enllaç extern"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
@@ -904,15 +908,15 @@ msgstr "Mostra el menú natiu (escriptori)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
msgstr "Mostra la icona d'estrella"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
msgstr "Mostra el recompte de no llegits a la icona de favorits de la pestanya"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
msgstr "Mostra el recompte de no llegits al títol de la pestanya"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -982,7 +986,7 @@ msgstr "Etiquetes"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Teal"
|
||||
msgstr ""
|
||||
msgstr "Blau verdós"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
@@ -1042,7 +1046,7 @@ msgstr "Nom d'usuari o correu electrònic"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Violet"
|
||||
msgstr ""
|
||||
msgstr "Violeta"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
@@ -1054,7 +1058,7 @@ msgstr "Lloc web"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Yellow"
|
||||
msgstr ""
|
||||
msgstr "Groc"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Potřebujete účet?</0><1>Zaregistrujte se!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Odkaz"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Načítání profilu..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Jméno"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Přejděte na předplatné zadáním jeho názvu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Angen cyfrif?</0><1>Ymunwch!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Cyswllt"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Wrthi'n llwytho proffil..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Enw"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Llywiwch i danysgrifiad trwy nodi ei enw"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Har du brug for en konto?</0><1>Tilmeld dig!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Indlæser profil..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Navn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviger til et abonnement ved at indtaste dets navn"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,8 +18,8 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr "<0>CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr "<0>Die vollständige Syntax ist </0><1>hier</1> verfügbar."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>Die vollständige Syntax ist </0><1>hier</1> verfügbar<2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -33,10 +33,6 @@ msgstr "<0>Hey,</0><1>Ich bin Jérémie aus Belgien und arbeite seit über 10 Ja
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Verbindung"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Lade Profil..."
|
||||
@@ -602,6 +602,10 @@ msgstr ""
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigieren Sie zu einem Abonnement, indem Sie seinen Namen eingeben"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,8 +18,8 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -33,10 +33,6 @@ msgstr "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaF
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Need an account?</0><1>Sign up!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr "Lime"
|
||||
msgid "Link"
|
||||
msgstr "Link"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr "Link to the documentation"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Loading profile..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Name"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigate to a subscription by entering its name"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -19,8 +19,8 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr "<0>CommaFeed es un proyecto de código abierto. El código fuente está hospedado en </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr "<0>La sintaxis completa está disponible </0><1>aquí</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>La sintaxis completa está disponible </0><1>aquí</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -34,10 +34,6 @@ msgstr "<0>Hola,</0><1>Soy Jérémie de Bélgica y he estado trabajando en Comma
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>¿Necesitas una cuenta?</0><1>¡Regístrate!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -517,6 +513,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Enlace"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Cargando perfil..."
|
||||
@@ -603,6 +603,10 @@ msgstr "Nombre"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegar a una suscripción introduciendo su nombre"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>به یک حساب نیاز دارید؟</0><1>ثبت نام کنید!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "پیوند"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "بارگیری نمایه..."
|
||||
@@ -602,6 +602,10 @@ msgstr "نام"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "با وارد کردن نام اشتراک، به آن بروید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Tarvitsetko tilin?</0><1>Rekisteröidy!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Linkki"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Ladataan profiilia..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Nimi"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Siirry tilaukseen kirjoittamalla sen nimi"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,8 +18,8 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr "<0>CommaFeed est un projet open-source. Les sources sont hébergées sur </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr "<0>La syntaxe complète est disponible </0><1>ici</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>La syntaxe complète est disponible </0><1>ici</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -33,10 +33,6 @@ msgstr "<0>Salut,</0><1>Je m'appelle Jérémie, je suis belge, et je développe
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Besoin d'un compte ?</0><1>Enregistrez-vous !</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr "Jaune-vert"
|
||||
msgid "Link"
|
||||
msgstr "Lien"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr "Lien vers la documentation"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Chargement du profil..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Nom"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviguer vers un abonnement en entrant son nom"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr "Aller vers la catégorie/le flux comportant des entrées non lues suivant après avoir marqué toutes les entrées lues"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Necesitas unha conta?</0><1>Rexístrate!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Ligazón"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Cargando perfil..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Nome"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navega a unha subscrición introducindo o seu nome"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Fiókra van szüksége?</0><1>Regisztráljon!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Profil betöltése..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Név"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigáljon egy előfizetéshez a nevének megadásával"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Butuh akun?</0><1>Daftar!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Tautan"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Memuat profil..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Nama"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigasikan ke langganan dengan memasukkan namanya"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -33,10 +33,6 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Hai bisogno di un account?</0><1>Registrati!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -516,6 +512,10 @@ msgstr ""
|
||||
msgid "Link"
|
||||
msgstr "Collegamento"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Link to the documentation"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Caricamento profilo..."
|
||||
@@ -602,6 +602,10 @@ msgstr "Nome"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigare verso un abbonamento inserendo il suo nome"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Navigate to the next category/feed with unread entries when marking all entries as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user