Compare commits

...

284 Commits

Author SHA1 Message Date
Athou
53f0c33c1d release 5.11.0 2025-08-06 22:51:34 +02:00
Jérémie Panzer
563516901e Merge pull request #1866 from Athou/renovate/major-github-artifact-actions
chore(deps): update actions/download-artifact action to v5
2025-08-06 10:41:11 +02:00
renovate[bot]
73b40fd8b7 chore(deps): update actions/download-artifact action to v5 2025-08-06 03:42:33 +00:00
renovate[bot]
08224a8486 fix(deps): update mantine monorepo to ^8.2.3 2025-08-06 03:42:29 +00:00
Jérémie Panzer
993f3d3aa8 Merge pull request #1865 from Athou/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2025-08-04 23:07:56 +02:00
renovate[bot]
3a975de136 chore(deps): lock file maintenance 2025-08-04 22:12:32 +02:00
renovate[bot]
48b5195798 chore(deps): update docker/login-action digest to 184bdaa 2025-08-04 16:28:14 +00:00
Jérémie Panzer
8eb34c7539 Merge pull request #1864 from Athou/renovate/linguijs-monorepo
fix(deps): update linguijs monorepo to ^5.4.0 (minor)
2025-08-01 17:41:33 +02:00
renovate[bot]
d75d7a9209 fix(deps): update linguijs monorepo to ^5.4.0 2025-08-01 14:49:38 +00:00
Jérémie Panzer
ddf851f1eb Merge pull request #1863 from Athou/renovate/node-22.x
chore(deps): update node.js to v22.18.0
2025-08-01 07:14:50 +02:00
renovate[bot]
889dd00c23 chore(deps): update node.js to v22.18.0 2025-08-01 03:24:10 +00:00
Jérémie Panzer
c5ea2a1aa1 Merge pull request #1862 from Athou/renovate/typescript-5.x
chore(deps): update dependency typescript to ^5.9.2
2025-08-01 00:00:50 +02:00
renovate[bot]
1489aff78e chore(deps): update dependency typescript to ^5.9.2 2025-07-31 21:37:49 +00:00
renovate[bot]
640296d42f chore(deps): update dependency npm to v11.5.2 2025-07-30 21:41:05 +00:00
renovate[bot]
3b12b2a5f6 fix(deps): update mantine monorepo to ^8.2.2 2025-07-30 17:02:31 +00:00
Jérémie Panzer
d5c41a5167 Merge pull request #1861 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.25.0 (minor)
2025-07-30 14:11:33 +02:00
Athou
58bf86d25d remove warnings 2025-07-30 13:30:27 +02:00
renovate[bot]
d73034d6d9 fix(deps): update quarkus.version to v3.25.0 2025-07-30 10:46:35 +00:00
renovate[bot]
151a613dcc chore(deps): update github/codeql-action digest to 51f7732 2025-07-30 00:24:09 +00:00
renovate[bot]
4bb741a42f chore(deps): update dependency @biomejs/biome to v2.1.3 2025-07-29 14:31:22 +00:00
renovate[bot]
cc9c8d3db3 chore(deps): update dependency vite-plugin-checker to ^0.10.2 2025-07-29 13:47:55 +00:00
Athou
c3d4831550 fix sonar warnings 2025-07-29 15:28:52 +02:00
renovate[bot]
31e385fbfb chore(deps): update react monorepo 2025-07-29 10:41:04 +00:00
Athou
a8c47d717c remove unused google analytics support 2025-07-29 12:35:53 +02:00
Athou
9a25157d3f make sonar scan client files too 2025-07-29 10:28:22 +02:00
renovate[bot]
9176e0f7b7 fix(deps): update react monorepo to ^19.1.1 2025-07-28 20:24:38 +00:00
Athou
ff7aa890a6 fix build warning 2025-07-28 19:55:36 +02:00
Jérémie Panzer
03312c1592 Merge pull request #1855 from Athou/renovate/patch-testing-library-monorepo
chore(deps): update dependency @testing-library/jest-dom to ^6.6.4
2025-07-28 09:38:33 +02:00
renovate[bot]
9d1ec2c636 chore(deps): update dependency @testing-library/jest-dom to ^6.6.4 2025-07-28 06:35:05 +00:00
Athou
c49c31a44e let quarkus generate the documentation 2025-07-28 08:33:34 +02:00
renovate[bot]
947c1f562f chore(deps): lock file maintenance 2025-07-28 03:57:26 +00:00
Jérémie Panzer
2d1dbb6988 Merge pull request #1857 from aniol/patch-2
Update messages.po
2025-07-27 17:45:19 +02:00
Aniol
622e46ff67 Update messages.po 2025-07-27 11:46:17 +02:00
renovate[bot]
4ff45a65c3 chore(deps): update ibm-semeru-runtimes docker tag to open-21.0.8_9-jre 2025-07-25 22:54:42 +00:00
Jérémie Panzer
a62676061b Merge pull request #1853 from Athou/renovate/patch-react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.7.1
2025-07-25 11:49:59 +02:00
renovate[bot]
11d77d2265 fix(deps): update dependency react-router-dom to ^7.7.1 2025-07-25 11:21:11 +02:00
Jérémie Panzer
1e7d44b250 Merge pull request #1852 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11.5.1
2025-07-24 23:01:56 +02:00
renovate[bot]
ffd86c6d8c chore(deps): update dependency npm to v11.5.1 2025-07-24 20:10:26 +00:00
renovate[bot]
a566c9460d chore(deps): update dependency vite to ^7.0.6 2025-07-24 06:37:44 +00:00
renovate[bot]
24edae3d58 fix(deps): update quarkus.version to v3.24.5 2025-07-23 22:10:27 +00:00
renovate[bot]
97876344c4 chore(deps): update github/codeql-action digest to 4e828ff 2025-07-23 18:03:54 +00:00
Jérémie Panzer
95dbeb9a47 Merge pull request #1851 from Athou/renovate/axios-1.x
fix(deps): update dependency axios to ^1.11.0
2025-07-23 20:03:14 +02:00
renovate[bot]
3fc64859b1 fix(deps): update dependency axios to ^1.11.0 2025-07-23 12:47:09 +00:00
renovate[bot]
896fe3b5b2 chore(deps): update graalvm/setup-graalvm digest to 7f488cf 2025-07-23 12:46:47 +00:00
Jérémie Panzer
85404781a3 Merge pull request #1850 from Athou/renovate/debian-12.11
chore(deps): update debian:12.11 docker digest to b6507e3
2025-07-22 11:39:19 +02:00
renovate[bot]
efe2abc86e chore(deps): update debian:12.11 docker digest to b6507e3 2025-07-22 07:58:53 +00:00
renovate[bot]
b70b7a0b40 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.46.1 2025-07-21 22:55:20 +00:00
Athou
865c80f87b add more integration tests 2025-07-21 20:21:20 +02:00
Athou
23a91aab12 providers can now return multiple urls 2025-07-21 16:57:10 +02:00
Athou
085a3cbb50 unwanted entries are already filtered at the dao level 2025-07-21 16:57:10 +02:00
Athou
fb9d875c31 extract image proxying from FeedUtils 2025-07-21 16:57:10 +02:00
Athou
5ee15c6f68 we no longer need to generate OPML 1.1 files 2025-07-21 16:57:10 +02:00
Athou
9853205849 remove workaround for #39 as it's not valid rdf 0.9 according to the spec 2025-07-21 16:57:10 +02:00
Athou
2c9ce7e8fc remove workaround for #140 that is no longer needed 2025-07-21 16:57:10 +02:00
Jérémie Panzer
9753ae60e2 Merge pull request #1848 from Athou/renovate/mantine-monorepo
fix(deps): update mantine monorepo to ^8.2.1 (minor)
2025-07-21 16:57:04 +02:00
renovate[bot]
bd66f1e682 fix(deps): update mantine monorepo to ^8.2.1 2025-07-21 14:20:33 +00:00
renovate[bot]
ed6a45c119 chore(deps): update dependency vite-plugin-checker to ^0.10.1 2025-07-21 14:20:25 +00:00
renovate[bot]
8f53ce27fc chore(deps): update github/codeql-action digest to d6bbdef 2025-07-21 12:47:08 +00:00
Athou
f7ae2e6689 add even more integration tests 2025-07-21 11:50:10 +02:00
Jérémie Panzer
c6cc47192c Merge pull request #1847 from Athou/renovate/com.diffplug.spotless-spotless-maven-plugin-2.x
chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.46.0
2025-07-21 07:39:56 +02:00
renovate[bot]
1c447fe369 chore(deps): lock file maintenance 2025-07-21 03:02:54 +00:00
renovate[bot]
6b5c92db48 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.46.0 2025-07-20 22:32:09 +00:00
Athou
427e020d27 add category integration tests 2025-07-20 23:20:30 +02:00
Athou
18084995b2 fix integration tests code coverage not taken into account 2025-07-20 09:58:01 +02:00
Athou
f894fdf564 extract url utils from FeedUtils 2025-07-20 07:26:43 +02:00
Athou
0b0a964a90 enums are now supported by openapi, allowableValues is no longer required 2025-07-20 01:08:27 +02:00
Athou
d6df979d0d remove more warnings 2025-07-19 23:47:22 +02:00
renovate[bot]
c366c37afe fix(deps): update dependency tss-react to ^4.9.19 2025-07-19 10:06:55 +00:00
Jérémie Panzer
20cbd239b2 Merge pull request #1846 from Athou/alert-autofix-26
fix for code scanning alert no. 26: Workflow does not contain permissions
2025-07-19 12:06:00 +02:00
Jérémie Panzer
a9c7595ee7 Potential fix for code scanning alert no. 26: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-19 11:58:48 +02:00
Athou
3f09e3ca64 remove warnings 2025-07-19 11:33:38 +02:00
Jérémie Panzer
ed42db7a0d Merge pull request #1845 from Athou/renovate/pin-dependencies
chore(deps): pin actions/setup-java action to c5195ef
2025-07-19 11:14:58 +02:00
renovate[bot]
c85daeb46e chore(deps): pin actions/setup-java action to c5195ef 2025-07-19 09:13:57 +00:00
Athou
3f2b93f1f8 run sonarqube as part of the CI 2025-07-19 11:13:10 +02:00
Athou
78d2e66c56 generate code coverage 2025-07-19 09:19:14 +02:00
Athou
0f2de651ff remove more warnings 2025-07-18 23:07:40 +02:00
Jérémie Panzer
2eb7c7237e Merge pull request #1844 from Athou/sonar-warnings
fix sonar warnings
2025-07-18 22:06:04 +02:00
Athou
3b8f62ff11 fix sonar warnings 2025-07-18 21:49:00 +02:00
Athou
f8bf9370de update SECURITY.md 2025-07-18 09:29:31 +02:00
Jérémie Panzer
30cd0ec089 Merge pull request #1843 from Athou/renovate/vitejs-plugin-react-4.x
chore(deps): update dependency @vitejs/plugin-react to ^4.7.0
2025-07-18 09:04:59 +02:00
Athou
e984be9289 update actions 2025-07-18 08:56:35 +02:00
Jérémie Panzer
8069787754 Merge pull request #1842 from Athou/renovate/pin-dependencies
chore(deps): pin github/codeql-action action to 181d5ee
2025-07-18 08:54:15 +02:00
renovate[bot]
343e442dff chore(deps): update dependency @vitejs/plugin-react to ^4.7.0 2025-07-18 06:35:48 +00:00
renovate[bot]
313ccdeae9 chore(deps): pin github/codeql-action action to 181d5ee 2025-07-18 06:35:21 +00:00
Jérémie Panzer
fdec8ebfd3 Create scorecard.yml 2025-07-18 08:34:40 +02:00
renovate[bot]
efddd86263 fix(deps): update quarkus.version to v3.24.4 2025-07-17 19:53:26 +00:00
renovate[bot]
7d18bde40b chore(deps): update dependency @biomejs/biome to v2.1.2 2025-07-17 15:58:18 +00:00
renovate[bot]
7fee410be4 chore(deps): update dependency vite to ^7.0.5 2025-07-17 05:32:53 +00:00
Jérémie Panzer
ebc2516a53 Merge pull request #1841 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.7.0
2025-07-17 07:31:48 +02:00
renovate[bot]
ade4d1d782 fix(deps): update dependency react-router-dom to ^7.7.0 2025-07-16 20:58:30 +00:00
renovate[bot]
07f7a288d2 chore(deps): update ibm-semeru-runtimes:open-21.0.7_6-jre docker digest to a51c2a5 2025-07-16 16:43:24 +00:00
Jérémie Panzer
380ed16caf Merge pull request #1840 from Athou/renovate/node-22.17.x
chore(deps): update node.js to v22.17.1
2025-07-16 14:05:42 +02:00
renovate[bot]
db654a10d1 chore(deps): update node.js to v22.17.1 2025-07-16 06:49:48 +00:00
renovate[bot]
2cf84d35cd chore(deps): update dependency maven to v3.9.11 2025-07-16 05:03:29 +00:00
renovate[bot]
a4eac86913 chore(deps): update ibm-semeru-runtimes:open-21.0.7_6-jre docker digest to 7b6e1ee 2025-07-16 03:04:56 +00:00
renovate[bot]
5168be45a8 chore(deps): lock file maintenance 2025-07-14 00:41:04 +00:00
Athou
163ab43da3 lingui documentation mentions that their plugin should be declared before the react compiler plugin 2025-07-11 16:56:06 +02:00
Jérémie Panzer
e5fa517270 Merge pull request #1830 from Athou/renovate/vite-7.x
chore(deps): update dependency vite to v7
2025-07-11 16:15:48 +02:00
renovate[bot]
b8b8ea5ce2 chore(deps): update dependency vite to v7 2025-07-11 13:50:21 +00:00
renovate[bot]
991b147af5 fix(deps): update linguijs monorepo to ^5.3.3 2025-07-11 13:25:53 +00:00
renovate[bot]
ecff62d0fa fix(deps): update quarkus.version to v3.24.3 2025-07-09 17:05:02 +00:00
renovate[bot]
cdec4c0879 chore(deps): update dependency @biomejs/biome to v2.1.1 2025-07-08 17:03:55 +00:00
Jérémie Panzer
e8085ac4cf Merge pull request #1838 from Athou/renovate/biomejs-biome-2.x
chore(deps): update dependency @biomejs/biome to v2.1.0
2025-07-08 14:00:21 +02:00
renovate[bot]
327062112b chore(deps): update dependency @biomejs/biome to v2.1.0 2025-07-08 11:36:19 +00:00
Jérémie Panzer
6dfc23c33a Merge pull request #1837 from Athou/renovate/com.diffplug.spotless-spotless-maven-plugin-2.x
chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.45.0
2025-07-08 07:16:20 +02:00
renovate[bot]
a601b0ab35 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.45.0 2025-07-08 03:07:19 +00:00
Jérémie Panzer
a48c8ca87a Merge pull request #1836 from Athou/renovate/vite-plugin-checker-0.x
chore(deps): update dependency vite-plugin-checker to ^0.10.0
2025-07-07 22:25:17 +02:00
renovate[bot]
b59e64a3d1 chore(deps): update dependency vite-plugin-checker to ^0.10.0 2025-07-07 16:57:19 +00:00
renovate[bot]
5fc62dd06d fix(deps): update mantine monorepo to ^8.1.3 2025-07-07 16:57:10 +00:00
renovate[bot]
d81a0cae91 chore(deps): lock file maintenance 2025-07-07 01:52:36 +00:00
Jérémie Panzer
50e31c6b69 Merge pull request #1834 from Athou/renovate/patch-quarkus.version
fix(deps): update quarkus.version to v3.24.2 (patch)
2025-07-02 22:36:15 +02:00
Jérémie Panzer
92d3d88127 Merge pull request #1833 from Athou/renovate/ibm-semeru-runtimes-open-21.0.7_6-jre
chore(deps): update ibm-semeru-runtimes:open-21.0.7_6-jre docker digest to 17a67d7
2025-07-02 22:36:09 +02:00
renovate[bot]
517fdb2095 fix(deps): update quarkus.version to v3.24.2 2025-07-02 11:42:24 +00:00
renovate[bot]
d16ebb02b4 chore(deps): update ibm-semeru-runtimes:open-21.0.7_6-jre docker digest to 17a67d7 2025-07-02 11:42:21 +00:00
renovate[bot]
a5c64c8b7b chore(deps): update debian:12.11 docker digest to d42b86d 2025-07-01 08:39:24 +00:00
renovate[bot]
5287a93484 chore(deps): update dependency io.quarkiverse.playwright:quarkus-playwright to v2.1.3 2025-06-30 15:45:38 +00:00
renovate[bot]
06e84d9032 chore(deps): lock file maintenance 2025-06-30 06:50:33 +00:00
Jérémie Panzer
3a63dd032a Merge pull request #1832 from Athou/renovate/io.quarkiverse.playwright-quarkus-playwright-2.1.x
chore(deps): update dependency io.quarkiverse.playwright:quarkus-playwright to v2.1.2
2025-06-30 08:48:51 +02:00
renovate[bot]
889e227523 chore(deps): update dependency io.quarkiverse.playwright:quarkus-playwright to v2.1.2 2025-06-30 06:12:02 +00:00
Athou
57d895daf5 fix playwright tests 2025-06-30 08:08:01 +02:00
renovate[bot]
a744394faa chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.26.1 2025-06-30 01:01:56 +00:00
renovate[bot]
64e3c25bad chore(deps): update ncipollo/release-action digest to bcfe547 2025-06-29 21:05:27 +00:00
renovate[bot]
75f85e1fb2 chore(deps): update ncipollo/release-action digest to 9128f23 2025-06-29 02:27:25 +00:00
renovate[bot]
00bd4cab37 fix(deps): update dependency react-router-dom to ^7.6.3 2025-06-27 22:26:32 +00:00
Athou
a9527f59a9 add @/ prefix for absolute imports 2025-06-27 16:29:31 +02:00
renovate[bot]
77661930f0 chore(deps): update dependency @biomejs/biome to v2.0.6 2025-06-27 13:14:09 +00:00
Athou
80a09bd9a0 use quarkus-playwright to simplify tests 2025-06-26 21:30:29 +02:00
Jérémie Panzer
6eb7cfbdc2 Merge pull request #1831 from Athou/renovate/react-draggable-4.x
fix(deps): update dependency react-draggable to ^4.5.0
2025-06-26 06:11:08 +02:00
renovate[bot]
fb186530aa fix(deps): update dependency react-draggable to ^4.5.0 2025-06-25 20:36:05 +00:00
renovate[bot]
6c121ccb90 fix(deps): update mantine monorepo to ^8.1.2 2025-06-25 17:40:13 +00:00
renovate[bot]
c08ad3b365 chore(deps): update graalvm/setup-graalvm digest to e1df20a 2025-06-25 17:00:47 +00:00
Jérémie Panzer
1668bc88ad Merge pull request #1828 from Athou/renovate/node-22.x
chore(deps): update node.js to v22.17.0
2025-06-25 19:00:06 +02:00
Jérémie Panzer
3a43f62460 Merge pull request #1829 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.24.1 (minor)
2025-06-25 18:44:04 +02:00
renovate[bot]
bfba5179d1 fix(deps): update quarkus.version to v3.24.1 2025-06-25 15:15:03 +00:00
renovate[bot]
78bf7856dc chore(deps): update node.js to v22.17.0 2025-06-25 15:14:58 +00:00
Jérémie Panzer
e0c708f677 Merge pull request #1827 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.26.0
2025-06-25 17:13:36 +02:00
renovate[bot]
794d6824e8 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.26.0 2025-06-25 12:44:58 +00:00
renovate[bot]
15573a7bee chore(deps): update dependency @biomejs/biome to v2.0.5 2025-06-23 16:46:39 +00:00
Jérémie Panzer
31c61a79c6 Merge pull request #1824 from Athou/renovate/org.jsoup-jsoup-1.x
fix(deps): update dependency org.jsoup:jsoup to v1.21.1
2025-06-23 08:18:44 +02:00
Jérémie Panzer
87ca427094 Merge pull request #1823 from Athou/renovate/vitejs-plugin-react-4.x
chore(deps): update dependency @vitejs/plugin-react to ^4.6.0
2025-06-23 08:18:30 +02:00
renovate[bot]
99bdc904e0 fix(deps): update dependency org.jsoup:jsoup to v1.21.1 2025-06-23 05:28:26 +00:00
renovate[bot]
2fdee68feb chore(deps): update dependency @vitejs/plugin-react to ^4.6.0 2025-06-23 05:28:23 +00:00
renovate[bot]
7be014f83e chore(deps): lock file maintenance 2025-06-23 02:43:32 +00:00
renovate[bot]
5668fe0a33 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.25.1 2025-06-21 16:33:29 +00:00
Jérémie Panzer
32c07efe19 Merge pull request #1821 from Athou/renovate/biomejs-biome-2.0.x
chore(deps): update dependency @biomejs/biome to v2.0.4
2025-06-21 14:03:28 +02:00
renovate[bot]
21b23d0f79 chore(deps): update dependency @biomejs/biome to v2.0.4 2025-06-21 10:03:08 +00:00
Jérémie Panzer
793d0dd13f Merge pull request #1822 from c-cesar/patch-1
Update pt translations
2025-06-21 09:53:12 +02:00
ccesar
14e8ff4c1b Update pt translations
Some fixes and translated the remaining strings.
2025-06-20 23:51:38 -03:00
renovate[bot]
416ab06997 fix(deps): update swagger.version to v2.2.34 2025-06-20 12:09:56 +00:00
renovate[bot]
493cd60dae fix(deps): update dependency io.dropwizard.metrics:metrics-json to v4.2.33 2025-06-20 00:44:19 +00:00
Jérémie Panzer
e0948e1e9e Merge pull request #1819 from Athou/renovate/patch-quarkus.version
fix(deps): update quarkus.version to v3.23.4 (patch)
2025-06-20 00:21:57 +02:00
renovate[bot]
5776b8c044 fix(deps): update quarkus.version to v3.23.4 2025-06-19 16:55:06 +00:00
Athou
38ab4105d8 noUnusedImports and noUnusedVariables are now enabled by default 2025-06-18 20:46:56 +02:00
renovate[bot]
5ed9dadcc2 chore(deps): update docker/setup-buildx-action digest to e468171 2025-06-18 14:10:38 +00:00
renovate[bot]
357d7e2381 chore(deps): update ibm-semeru-runtimes:open-21.0.7_6-jre docker digest to c5b834e 2025-06-17 23:51:09 +00:00
renovate[bot]
8cfaab3e9f chore(deps): update dependency vitest to ^3.2.4 2025-06-17 18:15:47 +00:00
Athou
fef2404357 update biome to v2 2025-06-17 20:14:38 +02:00
renovate[bot]
1aa1bce8c8 fix(deps): update mantine monorepo to ^8.1.1 2025-06-16 22:14:42 +00:00
renovate[bot]
124b2761f6 chore(deps): update docker/setup-buildx-action digest to 18ce135 2025-06-16 17:11:47 +00:00
renovate[bot]
066ca1af7c chore(deps): lock file maintenance 2025-06-16 02:29:40 +00:00
Jérémie Panzer
c20520879b Merge pull request #1817 from Athou/renovate/axios-1.x
fix(deps): update dependency axios to ^1.10.0
2025-06-14 17:35:48 +02:00
renovate[bot]
4fa5b2b856 fix(deps): update dependency axios to ^1.10.0 2025-06-14 14:05:49 +00:00
renovate[bot]
5c1b1fad76 fix(deps): update swagger.version to v2.2.33 2025-06-14 09:46:23 +00:00
renovate[bot]
c18d248c06 chore(deps): update dependency npm to v11.4.2 2025-06-14 07:26:37 +00:00
Athou
d46ee7f673 remove deprecated usage of saveOrUpdate in preparation of hibernate 7 2025-06-13 14:49:22 +02:00
renovate[bot]
f2c0d99bd9 fix(deps): update quarkus.version to v3.23.3 2025-06-12 00:04:22 +00:00
renovate[bot]
60ee0b9185 chore(deps): update dependency @types/react to ^19.1.8 2025-06-11 17:51:58 +00:00
renovate[bot]
4b3e660ae7 chore(deps): update debian:12.11 docker digest to 0d8498a 2025-06-11 03:06:07 +00:00
renovate[bot]
0b42392bfc chore(deps): update dependency vitest to ^3.2.3 2025-06-10 07:21:05 +00:00
renovate[bot]
a94d7ce235 chore(deps): update dependency @vitejs/plugin-react to ^4.5.2 2025-06-10 06:24:42 +00:00
Jérémie Panzer
72aec432ed Merge pull request #1814 from WangLei1993/master
add Chinese translation for new entry
2025-06-10 08:23:18 +02:00
WangLei1993
0e5db8d604 add Chinese translation for new entry 2025-06-10 13:34:56 +08:00
Jérémie Panzer
dc45fb4b84 Merge pull request #1812 from Athou/renovate/mantine-monorepo
fix(deps): update mantine monorepo to ^8.1.0 (minor)
2025-06-10 07:06:50 +02:00
Jérémie Panzer
6503d38fe3 Merge pull request #1813 from canoine/patch-5
Update fr/messages.po
2025-06-10 07:06:35 +02:00
canoine
32c89d9a11 Update fr/messages.po
Translation of the new messages.
2025-06-10 06:04:09 +02:00
renovate[bot]
f279465750 fix(deps): update mantine monorepo to ^8.1.0 2025-06-09 21:31:32 +00:00
renovate[bot]
58ec1b022a chore(deps): update dependency @types/react to ^19.1.7 2025-06-09 21:31:12 +00:00
Athou
612199429e fix custom css code documentation link (#1811) 2025-06-09 22:36:17 +02:00
Jérémie Panzer
e5482f9051 Merge pull request #1810 from Athou/renovate/major-querydsl.version
fix(deps): update querydsl.version to v7 (major)
2025-06-09 18:31:14 +02:00
renovate[bot]
05df14fda2 fix(deps): update querydsl.version to v7 2025-06-09 15:52:21 +00:00
renovate[bot]
29898ba1ba fix(deps): update dependency @fontsource/open-sans to ^5.2.6 2025-06-09 08:55:10 +00:00
renovate[bot]
93d1cec503 chore(deps): update dependency rollup-plugin-visualizer to ^6.0.3 2025-06-07 14:00:43 +00:00
renovate[bot]
9884f44122 fix(deps): update dependency style-to-object to ^1.0.9 2025-06-07 02:54:31 +00:00
renovate[bot]
d400456685 chore(deps): update dependency vitest to ^3.2.2 2025-06-06 23:49:01 +00:00
renovate[bot]
c39069cafd chore(deps): update dependency maven to v3.9.10 2025-06-06 12:01:14 +00:00
Jérémie Panzer
5fb0edc318 Merge pull request #1808 from Athou/renovate/patch-quarkus.version
fix(deps): update quarkus.version to v3.23.2 (patch)
2025-06-05 07:06:42 +02:00
renovate[bot]
21a6b2d780 fix(deps): update quarkus.version to v3.23.2 2025-06-04 22:32:20 +00:00
renovate[bot]
40c9063a54 chore(deps): update dependency @types/react-dom to ^19.1.6 2025-06-04 15:57:40 +00:00
Athou
59b0103ed5 add an option to navigate to the next unread category/feed when marking all as read (#1807) 2025-06-04 09:10:03 +02:00
Athou
f4730e9338 redirect to 'all' if no unread categories or feeds found or if we reached the end of the list (#1807) 2025-06-04 08:40:22 +02:00
Athou
b7b520ca3c faster integration tests execution by truncating tables instead of dropping and recreating tables 2025-06-04 08:35:03 +02:00
renovate[bot]
21d44e6a55 fix(deps): update dependency react-router-dom to ^7.6.2 2025-06-04 00:47:19 +00:00
renovate[bot]
607886f0f0 chore(deps): update dependency vitest to ^3.2.1 2025-06-03 18:19:26 +00:00
Jérémie Panzer
7cd3c68256 Merge pull request #1806 from Athou/renovate/vitejs-plugin-react-4.5.x
chore(deps): update dependency @vitejs/plugin-react to ^4.5.1
2025-06-03 12:02:04 +02:00
renovate[bot]
6e37c1bd86 chore(deps): update dependency @vitejs/plugin-react to ^4.5.1 2025-06-03 06:04:05 +00:00
Jérémie Panzer
5db1a0748f Merge pull request #1805 from Athou/renovate/vitest-monorepo
chore(deps): update dependency vitest to ^3.2.0
2025-06-02 20:20:24 +02:00
renovate[bot]
a7584df4f4 chore(deps): update dependency vitest to ^3.2.0 2025-06-02 13:57:05 +00:00
Athou
4421197403 remove unused variable 2025-06-02 07:42:19 +02:00
renovate[bot]
15b59467fb chore(deps): lock file maintenance 2025-06-02 01:17:56 +00:00
Jérémie Panzer
c95ff0a2ce Merge pull request #1804 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.25.0
2025-05-31 20:48:46 +02:00
renovate[bot]
7eff9df025 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.25.0 2025-05-31 18:22:52 +00:00
Athou
2f05e53e14 remove feed/refresh rest endpoint as it's unused and does not honor the force-refresh-cooldown-duration setting (#1802) 2025-05-30 11:19:26 +02:00
renovate[bot]
6089fe4036 fix(deps): update linguijs monorepo to ^5.3.2 2025-05-28 14:38:20 +00:00
renovate[bot]
10d9af0d86 chore(deps): update dependency rollup-plugin-visualizer to ^6.0.1 2025-05-28 13:12:40 +00:00
Jérémie Panzer
c119d5062a Merge pull request #1800 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.23.0 (minor)
2025-05-28 15:11:46 +02:00
renovate[bot]
324609ee60 fix(deps): update quarkus.version to v3.23.0 2025-05-28 11:39:17 +00:00
renovate[bot]
a0a65f2b45 chore(deps): update dependency org.codehaus.mojo:exec-maven-plugin to v3.5.1 2025-05-28 07:28:55 +00:00
renovate[bot]
45e5ca704c chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.5 2025-05-28 05:10:12 +00:00
Athou
f361be0c72 release 5.10.0 2025-05-28 07:08:11 +02:00
renovate[bot]
1611dc5703 chore(deps): update docker/build-push-action digest to 2634353 2025-05-27 20:52:00 +00:00
renovate[bot]
04faad84a4 fix(deps): update mantine monorepo to ^8.0.2 2025-05-27 14:24:50 +00:00
renovate[bot]
19c42e5838 chore(deps): update dependency @types/react to ^19.1.6 2025-05-27 11:59:22 +00:00
Athou
4918b69d0a improve performance by enabling the react compiler (#1087) 2025-05-26 21:06:53 +02:00
Athou
c7cec464aa improve performance by avoiding some big re-renders (#1087) 2025-05-26 20:55:27 +02:00
renovate[bot]
91857c4d73 chore(deps): lock file maintenance 2025-05-26 01:42:10 +00:00
Jérémie Panzer
fc6f9f4258 Merge pull request #1799 from Athou/renovate/patch-react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.6.1
2025-05-25 20:54:27 +02:00
renovate[bot]
34f9f9374a fix(deps): update dependency react-router-dom to ^7.6.1 2025-05-25 15:16:31 +00:00
renovate[bot]
0ae4c1621f fix(deps): update dependency io.dropwizard.metrics:metrics-json to v4.2.32 2025-05-25 01:30:25 +00:00
Athou
c393f5c045 improve aarch64 compatibility 2025-05-24 18:21:50 +02:00
Jérémie Panzer
1624290dc1 Merge pull request #1798 from Athou/renovate/rollup-plugin-visualizer-6.x
chore(deps): update dependency rollup-plugin-visualizer to v6
2025-05-24 16:31:45 +02:00
renovate[bot]
c6491990ac chore(deps): update dependency rollup-plugin-visualizer to v6 2025-05-24 13:46:34 +00:00
renovate[bot]
15dea17923 chore(deps): update dependency io.github.git-commit-id:git-commit-id-maven-plugin to v9.0.2 2025-05-23 21:45:42 +00:00
Athou
689d5ac7b2 clear indicator when entries are loaded 2025-05-23 22:37:28 +02:00
Athou
2142e20e7d cleanup 2025-05-23 16:03:16 +02:00
Jérémie Panzer
dc23126570 Merge pull request #1780 from Eshwar1212-maker/clean-red-dot
feat: red dot indicator for new unread articles
2025-05-23 15:54:22 +02:00
Jérémie Panzer
55856f9060 Merge pull request #1794 from Athou/renovate/vitejs-plugin-react-4.x
chore(deps): update dependency @vitejs/plugin-react to ^4.5.0
2025-05-23 09:20:28 +02:00
renovate[bot]
c756ce5fc8 chore(deps): update dependency @vitejs/plugin-react to ^4.5.0 2025-05-23 02:55:39 +00:00
Eshwar Tangirala
0546f25d55 Removed console.log 2025-05-22 20:13:16 -04:00
Eshwar Tangirala
7b33717333 Readjusted code to not use localstorage, and just used redux for indicator 2025-05-22 20:10:52 -04:00
Jérémie Panzer
6ea6d16e58 Merge pull request #1793 from Athou/renovate/org.apache.httpcomponents.client5-httpclient5-5.x
fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.5
2025-05-22 22:13:31 +02:00
renovate[bot]
a9b65c83aa fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.5 2025-05-22 17:14:03 +00:00
renovate[bot]
a497802b50 chore(deps): update dependency npm to v11.4.1 2025-05-22 05:05:19 +00:00
Jérémie Panzer
42b0428b9a Merge pull request #1792 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.24.0
2025-05-22 07:04:34 +02:00
Jérémie Panzer
931c553e1d Merge pull request #1791 from Athou/renovate/debian-12.x
chore(deps): update debian docker tag to v12.11
2025-05-22 07:03:52 +02:00
renovate[bot]
f3c0b92a3c chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.24.0 2025-05-22 03:10:15 +00:00
renovate[bot]
970cabf241 chore(deps): update debian docker tag to v12.11 2025-05-22 03:10:12 +00:00
renovate[bot]
e321ecde5d chore(deps): update dependency @types/react to ^19.1.5 2025-05-21 17:09:30 +00:00
Jérémie Panzer
32ac326a77 Merge pull request #1790 from Athou/renovate/node-22.x
chore(deps): update node.js to v22.16.0
2025-05-21 19:08:34 +02:00
renovate[bot]
134dcd4466 chore(deps): update node.js to v22.16.0 2025-05-21 16:41:07 +00:00
renovate[bot]
26a44353d4 chore(deps): update dependency vitest to ^3.1.4 2025-05-19 18:09:09 +00:00
renovate[bot]
55acb3ef28 chore(deps): lock file maintenance 2025-05-19 03:45:55 +00:00
Athou
0e96307726 ignore scheme case 2025-05-18 17:40:06 +02:00
Eshwar Tangirala
0199a36238 Working on indicator feature for new unread feeds 2025-05-18 00:03:28 -04:00
Eshwar Tangirala
3f2f6e83fa Adding red dot indicator feature, got main components done 2025-05-15 20:19:05 -04:00
renovate[bot]
4fa780cac2 chore(deps): update docker/build-push-action digest to 1dc7386 2025-05-15 21:12:24 +00:00
Jérémie Panzer
edb0f655b0 Merge pull request #1786 from Athou/renovate/patch-quarkus.version
fix(deps): update quarkus.version to v3.22.3 (patch)
2025-05-15 23:11:52 +02:00
Jérémie Panzer
651ada7073 Merge pull request #1787 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11.4.0
2025-05-15 23:11:36 +02:00
renovate[bot]
efb5d49d04 chore(deps): update dependency npm to v11.4.0 2025-05-15 20:08:41 +00:00
renovate[bot]
f78cc18b06 fix(deps): update quarkus.version to v3.22.3 2025-05-15 12:39:28 +00:00
renovate[bot]
8acffa11e5 fix(deps): update swagger.version to v2.2.32 2025-05-15 03:27:38 +00:00
renovate[bot]
f4246807ff chore(deps): update node.js to v22.15.1 2025-05-14 22:48:58 +00:00
renovate[bot]
abf6e7131b fix(deps): update dependency @reduxjs/toolkit to ^2.8.2 2025-05-14 19:06:20 +00:00
renovate[bot]
b2688520cc fix(deps): update mantine monorepo to ^8.0.1 2025-05-14 14:57:49 +00:00
Athou
fad0aea108 release 5.9.0 2025-05-14 16:56:01 +02:00
renovate[bot]
0b63773c83 fix(deps): update swagger.version to v2.2.31 2025-05-13 19:10:00 +00:00
renovate[bot]
3ef28009ac chore(deps): update dependency @types/react-dom to ^19.1.5 2025-05-13 14:35:24 +00:00
renovate[bot]
8979e2b191 chore(deps): update dependency @types/react to ^19.1.4 2025-05-12 23:34:04 +00:00
Eshwar Tangirala
d6910aa1e8 Cleaned up UI for Indicator 2025-05-12 16:30:06 -04:00
Eshwar Tangirala
afc56c6053 feat: red dot indicator for new unread articles 2025-05-12 16:22:40 -04:00
Eshwar Tangirala
1bd504cbfb feat: red dot indicator for new unread articles 2025-05-12 16:22:40 -04:00
renovate[bot]
2c089ddb5e chore(deps): update dependency @types/react-dom to ^19.1.4 2025-05-12 15:39:00 +00:00
Athou
0b5245643a increase cache duration of static resources even more (#1782) 2025-05-12 15:11:05 +02:00
Athou
ae35d43f7f revert back to deploy documentation on release only 2025-05-12 09:57:01 +02:00
Athou
fe55682c9f force update pages this time only 2025-05-12 09:56:22 +02:00
Jérémie Panzer
0d3e6f17e2 Merge pull request #1783 from Athou/renovate/pin-dependencies
chore(deps): pin jaywcjlove/markdown-to-html-cli action to d2c8ffd
2025-05-12 09:54:23 +02:00
renovate[bot]
d5659c4278 chore(deps): lock file maintenance 2025-05-12 06:49:59 +00:00
renovate[bot]
69b87b9026 chore(deps): pin jaywcjlove/markdown-to-html-cli action to d2c8ffd 2025-05-12 06:49:09 +00:00
Athou
168bcd3a37 add reference to the custom css documentation 2025-05-12 08:48:16 +02:00
Athou
e3b6be0cd0 add documentation for custom CSS (#1757) 2025-05-12 07:51:38 +02:00
Athou
eeceda0ca8 increase static resources cache duration (#1782) 2025-05-11 18:12:15 +02:00
renovate[bot]
aa903039c8 chore(deps): update ibm-semeru-runtimes docker tag to open-21.0.7_6-jre 2025-05-09 20:05:00 +00:00
Jérémie Panzer
73d81d0cdb Merge pull request #1778 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.6.0
2025-05-08 20:11:28 +02:00
renovate[bot]
01fe539af6 fix(deps): update dependency react-router-dom to ^7.6.0 2025-05-08 17:13:56 +00:00
renovate[bot]
c08063ca57 fix(deps): update dependency @reduxjs/toolkit to ^2.8.1 2025-05-08 03:51:29 +00:00
renovate[bot]
60d4af2890 fix(deps): update quarkus.version to v3.22.2 2025-05-07 22:33:33 +00:00
renovate[bot]
6378f074a8 fix(deps): update dependency tss-react to ^4.9.18 2025-05-07 19:57:06 +00:00
Athou
5082ec86fd Merge branch 'custom-css' 2025-05-07 19:44:24 +02:00
Jérémie Panzer
6cff5bb099 Merge pull request #1777 from WangLei1993/master
add Chinese translation for new entry
2025-05-07 07:32:45 +02:00
WangLei1993
d54562d56f add Chinese translation for new entry 2025-05-07 12:06:56 +08:00
Jérémie Panzer
2b45a8fae5 Merge pull request #1776 from Athou/renovate/reduxjs-toolkit-2.x
fix(deps): update dependency @reduxjs/toolkit to ^2.8.0
2025-05-07 05:37:18 +02:00
renovate[bot]
8654df8994 fix(deps): update dependency @reduxjs/toolkit to ^2.8.0 2025-05-06 23:47:34 +00:00
renovate[bot]
4d5145c17e chore(deps): update dependency vite-plugin-checker to ^0.9.3 2025-05-06 18:09:24 +00:00
Athou
b5c197f499 add more css classes based on feedback 2025-05-06 19:26:03 +02:00
Athou
d417655a86 add css classes to elements to ease css customization 2025-05-06 19:04:39 +02:00
259 changed files with 5840 additions and 4122 deletions

View File

@@ -29,7 +29,7 @@ jobs:
# Setup # Setup
- name: Set up GraalVM - name: Set up GraalVM
uses: graalvm/setup-graalvm@01ed653ac833fe80569f1ef9f25585ba2811baab # v1 uses: graalvm/setup-graalvm@7f488cf82a3629ee755e4e97342c01d6bed318fa # v1
with: with:
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm" distribution: "graalvm"
@@ -44,18 +44,27 @@ jobs:
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }} run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
# Build pages # Build pages
- name: Copy generated markdown documentation to /documentation - name: Create pages directory structure
run: mkdir documentation && cp ./commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md ./documentation/README.md run: mkdir -p target/pages/documentation/custom-css
- name: Generate pages - name: Convert readme file to html
uses: wranders/markdown-to-pages-action@8d8a750832932ac785f5424c8c5543aa0b26bb9a # v1 uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
with: with:
token: ${{ secrets.GITHUB_TOKEN }} source: README.md
out_path: target/pages output: target/pages/index.html
files: |-
README.md - name: Convert config documentation to html
documentation/README.md uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
with:
source: commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md
output: target/pages/documentation/index.html
- name: Convert custom css documentation to html
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
with:
source: documentation/CUSTOMCSS.md
output: target/pages/documentation/custom-css/index.html
# Upload artifacts # Upload artifacts
- name: Upload cross-platform app - name: Upload cross-platform app
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
@@ -98,14 +107,14 @@ jobs:
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Install required packages - name: Install required packages
run: sudo apt-get install -y rename unzip run: sudo apt-get install -y rename unzip
# Prepare artifacts # Prepare artifacts
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with: with:
pattern: commafeed-${{ matrix.database }}-* pattern: commafeed-${{ matrix.database }}-*
path: ./artifacts path: ./artifacts
@@ -126,7 +135,7 @@ jobs:
# Docker # Docker
- name: Login to Container Registry - name: Login to Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
if: ${{ env.DOCKERHUB_USERNAME != '' }} if: ${{ env.DOCKERHUB_USERNAME != '' }}
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -134,7 +143,7 @@ jobs:
## build but don't push for PRs and renovate ## build but don't push for PRs and renovate
- name: Docker build - native - name: Docker build - native
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with: with:
context: . context: .
file: commafeed-server/src/main/docker/Dockerfile.native file: commafeed-server/src/main/docker/Dockerfile.native
@@ -142,7 +151,7 @@ jobs:
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
- name: Docker build - jvm - name: Docker build - jvm
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with: with:
context: . context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm file: commafeed-server/src/main/docker/Dockerfile.jvm
@@ -151,7 +160,7 @@ jobs:
## build and push tag ## build and push tag
- name: Docker build and push tag - native - 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' }} if: ${{ github.ref_type == 'tag' }}
with: with:
context: . context: .
@@ -163,7 +172,7 @@ jobs:
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }} athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
- name: Docker build and push tag - jvm - 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' }} if: ${{ github.ref_type == 'tag' }}
with: with:
context: . context: .
@@ -176,7 +185,7 @@ jobs:
## build and push master ## build and push master
- name: Docker build and push master - native - 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' }} if: ${{ github.ref_name == 'master' }}
with: with:
context: . context: .
@@ -186,7 +195,7 @@ jobs:
tags: athou/commafeed:master-${{ matrix.database }} tags: athou/commafeed:master-${{ matrix.database }}
- name: Docker build and push master - jvm - 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' }} if: ${{ github.ref_name == 'master' }}
with: with:
context: . context: .
@@ -211,7 +220,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with: with:
pattern: commafeed-* pattern: commafeed-*
path: ./artifacts path: ./artifacts
@@ -227,7 +236,7 @@ jobs:
version: ${{ github.ref_name }} version: ${{ github.ref_name }}
- name: Create GitHub release - name: Create GitHub release
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1 uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1
with: with:
name: CommaFeed ${{ github.ref_name }} name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }} body: ${{ steps.changelog_reader.outputs.changes }}

78
.github/workflows/scorecard.yml vendored Normal file
View 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
View 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

View File

@@ -15,4 +15,4 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
distributionType=only-script 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

View File

@@ -1,5 +1,23 @@
# Changelog # 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)
- Added a link in the README to the [documentation](https://athou.github.io/commafeed/documentation/custom-css/) of the new CSS classes
- Static resources are now cached for much longer (#1782)
## [5.8.0] ## [5.8.0]
- A color picker is now available on the settings page to change the orange accent of the application (#1598) - A color picker is now available on the settings page to change the orange accent of the application (#1598)

View File

@@ -17,6 +17,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc
- REST API - REST API
- Fever-compatible API for native mobile apps - Fever-compatible API for native mobile apps
- Can automatically mark articles as read based on user-defined rules - Can automatically mark articles as read based on user-defined rules
- Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
- [Browser extension](https://github.com/Athou/commafeed-browser-extension) - [Browser extension](https://github.com/Athou/commafeed-browser-extension)
- Compiles to native code for blazing fast startup and low memory usage - Compiles to native code for blazing fast startup and low memory usage
- Supports 4 databases - Supports 4 databases

View File

@@ -2,5 +2,4 @@
## Reporting a Vulnerability ## 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. 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
Thanks !

View File

@@ -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": { "formatter": {
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4, "indentWidth": 4,
@@ -13,15 +13,7 @@
"arrowParentheses": "asNeeded" "arrowParentheses": "asNeeded"
} }
}, },
"linter": {
"rules": {
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error"
}
}
},
"files": { "files": {
"ignore": ["dist", "node_modules", "target", "target-ide"] "includes": ["**", "!**/dist", "!**/node_modules", "!**/target", "!**/target-ide"]
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -16,69 +16,68 @@
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@fontsource/open-sans": "^5.2.5", "@fontsource/open-sans": "^5.2.6",
"@lingui/core": "^5.3.1", "@lingui/core": "^5.4.0",
"@lingui/react": "^5.3.1", "@lingui/react": "^5.4.0",
"@mantine/core": "^8.0.0", "@mantine/core": "^8.2.3",
"@mantine/form": "^8.0.0", "@mantine/form": "^8.2.3",
"@mantine/hooks": "^8.0.0", "@mantine/hooks": "^8.2.3",
"@mantine/modals": "^8.0.0", "@mantine/modals": "^8.2.3",
"@mantine/notifications": "^8.0.0", "@mantine/notifications": "^8.2.3",
"@mantine/spotlight": "^8.0.0", "@mantine/spotlight": "^8.2.3",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.8.2",
"axios": "^1.9.0", "axios": "^1.11.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"interweave": "^13.1.1", "interweave": "^13.1.1",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"react": "^19.1.0", "react": "^19.1.1",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^19.1.0", "react-dom": "^19.1.1",
"react-draggable": "^4.4.6", "react-draggable": "^4.5.0",
"react-ga4": "^2.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-infinite-scroller": "^1.2.6", "react-infinite-scroller": "^1.2.6",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.5.3", "react-router-dom": "^7.7.1",
"react-swipeable": "^7.0.2", "react-swipeable": "^7.0.2",
"redoc": "^2.5.0", "style-to-object": "^1.0.9",
"style-to-object": "^1.0.8",
"throttle-debounce": "^5.0.2", "throttle-debounce": "^5.0.2",
"tinycon": "^0.6.8", "tinycon": "^0.6.8",
"tss-react": "^4.9.17", "tss-react": "^4.9.19",
"websocket-heartbeat-js": "^1.1.3" "websocket-heartbeat-js": "^1.1.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^2.1.3",
"@lingui/babel-plugin-lingui-macro": "^5.3.1", "@lingui/babel-plugin-lingui-macro": "^5.4.0",
"@lingui/cli": "^5.3.1", "@lingui/cli": "^5.4.0",
"@lingui/vite-plugin": "^5.3.1", "@lingui/vite-plugin": "^5.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/mousetrap": "^1.6.15", "@types/mousetrap": "^1.6.15",
"@types/react": "^19.1.3", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^19.1.7",
"@types/react-infinite-scroller": "^1.2.5", "@types/react-infinite-scroller": "^1.2.5",
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@types/tinycon": "^0.6.7", "@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-macros": "^3.1.0",
"babel-plugin-react-compiler": "^19.1.0-rc.2",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"rollup-plugin-visualizer": "^5.14.0", "rollup-plugin-visualizer": "^6.0.3",
"typescript": "^5.8.3", "typescript": "^5.9.2",
"vite": "^6.3.5", "vite": "^7.0.6",
"vite-plugin-checker": "^0.9.2", "vite-plugin-checker": "^0.10.2",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3" "vitest": "^3.2.4"
}, },
"overrides": { "overrides": {
"react-infinite-scroller": { "react-infinite-scroller": {
"react": "^19.1.0" "react": "^19.1.1"
} }
} }
} }

View File

@@ -6,16 +6,19 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>5.8.0</version> <version>5.11.0</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>
<properties> <properties>
<sonar.sources>package.json,src</sonar.sources>
<sonar.coverage.exclusions>**/*</sonar.coverage.exclusions>
<!-- renovate: datasource=node-version depName=node --> <!-- 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 --> <!-- renovate: datasource=npm depName=npm -->
<npm.version>11.3.0</npm.version> <npm.version>11.5.2</npm.version>
</properties> </properties>
<build> <build>

View File

@@ -3,40 +3,44 @@ import { I18nProvider } from "@lingui/react"
import { MantineProvider } from "@mantine/core" import { MantineProvider } from "@mantine/core"
import { ModalsProvider } from "@mantine/modals" import { ModalsProvider } from "@mantine/modals"
import { Notifications } from "@mantine/notifications" import { Notifications } from "@mantine/notifications"
import { Constants } from "app/constants" import type React from "react"
import { redirectTo } from "app/redirect/slice" import { useEffect, useState } from "react"
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 { isSafari } from "react-device-detect" import { isSafari } from "react-device-detect"
import ReactGA from "react-ga4" import { HashRouter, Navigate, Route, Routes, useNavigate } from "react-router-dom"
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
import Tinycon from "tinycon" 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 const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
return ( return (
<I18nProvider i18n={i18n}> <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() { function AppRoutes() {
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible) const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
@@ -85,7 +86,6 @@ function AppRoutes() {
<Route path="login" element={<LoginPage />} /> <Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegistrationPage />} /> <Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} /> <Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="api" element={<ApiDocumentationPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarVisible={sidebarVisible} />}> <Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarVisible={sidebarVisible} />}>
<Route path="category"> <Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} /> <Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
@@ -128,26 +128,19 @@ function RedirectHandler() {
return null return null
} }
function GoogleAnalyticsHandler() { function UnreadCountTitleHandler({
const location = useLocation() enabled,
const googleAnalyticsCode = useAppSelector(state => state.server.serverInfos?.googleAnalyticsCode) }: Readonly<{
enabled?: boolean
useEffect(() => { }>) {
if (googleAnalyticsCode) ReactGA.initialize(googleAnalyticsCode) const root = useAppSelector(state => state.tree.rootCategory)
}, [googleAnalyticsCode]) const unreadCount = categoryUnreadCount(root)
useEffect(() => {
if (ReactGA.isInitialized) ReactGA.send({ hitType: "pageview", page: location.pathname })
}, [location])
return null
}
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title> 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(() => { useEffect(() => {
if (enabled && unreadCount > 0) { if (enabled && unreadCount > 0) {
Tinycon.setBubble(unreadCount) Tinycon.setBubble(unreadCount)
@@ -205,38 +198,32 @@ function CustomCssHandler() {
export function App() { export function App() {
useI18n() useI18n()
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle) const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon) const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const unreadCount = categoryUnreadCount(root)
useEffect(() => { useEffect(() => {
dispatch(reloadServerInfos()) dispatch(reloadServerInfos())
}, [dispatch]) }, [dispatch])
return ( return (
<Providers> <Providers>
<> <UnreadCountTitleHandler enabled={unreadCountTitle} />
<UnreadCountTitleHandler unreadCount={unreadCount} enabled={unreadCountTitle} /> <UnreadCountFaviconHandler enabled={unreadCountFavicon} />
<UnreadCountFaviconHandler unreadCount={unreadCount} enabled={unreadCountFavicon} /> <BrowserExtensionBadgeUnreadCountHandler />
<BrowserExtensionBadgeUnreadCountHandler /> <CustomJsHandler />
<CustomJsHandler /> <CustomCssHandler />
<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 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 https://github.com/Athou/commafeed/issues/1168
*/} */}
{!isSafari && <DisablePullToRefresh />} {!isSafari && <DisablePullToRefresh />}
<HashRouter> <HashRouter>
<GoogleAnalyticsHandler /> <RedirectHandler />
<RedirectHandler /> <AppRoutes />
<AppRoutes /> </HashRouter>
</HashRouter>
</>
</Providers> </Providers>
) )
} }

View File

@@ -1,5 +1,5 @@
import { createAsyncThunk } from "@reduxjs/toolkit" import { createAsyncThunk } from "@reduxjs/toolkit"
import type { AppDispatch, RootState } from "app/store" import type { AppDispatch, RootState } from "@/app/store"
export const createAppAsyncThunk = createAsyncThunk.withTypes<{ export const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState state: RootState

View File

@@ -105,7 +105,7 @@ export const client = {
}, },
admin: { admin: {
getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"), 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), deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req),
getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"), getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"),
}, },

View File

@@ -87,17 +87,15 @@ export const Constants = {
headerHeight: 60, headerHeight: 60,
entryMaxWidth: 650, entryMaxWidth: 650,
isTopVisible: (div: HTMLElement) => { 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) return div.getBoundingClientRect().top >= (header?.bottom ?? 0)
}, },
isBottomVisible: (div: HTMLElement) => { 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) return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight)
}, },
}, },
dom: { dom: {
headerId: "header",
footerId: "footer",
entryId: (entry: Entry) => `entry-id-${entry.id}`, entryId: (entry: Entry) => `entry-id-${entry.id}`,
entryContextMenuId: (entry: Entry) => entry.id, entryContextMenuId: (entry: Entry) => entry.id,
}, },
@@ -108,5 +106,6 @@ export const Constants = {
delay: 500, delay: 500,
}, },
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension", browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e", bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
} }

View File

@@ -1,12 +1,12 @@
import { configureStore } from "@reduxjs/toolkit" 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 type { AxiosResponse } from "axios"
import { beforeEach, describe, expect, it, vi } from "vitest" 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", () => { describe("entries", () => {
beforeEach(() => { beforeEach(() => {
@@ -27,7 +27,12 @@ describe("entries", () => {
} as AxiosResponse<Entries>) } as AxiosResponse<Entries>)
const store = configureStore({ reducer: reducers }) 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.type).toBe("feed")
expect(store.getState().entries.source.id).toBe("feed-id") expect(store.getState().entries.source.id).toBe("feed-id")
@@ -130,11 +135,19 @@ describe("entries", () => {
} as RootState, } 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([ expect(store.getState().entries.entries).toStrictEqual([
{ id: "3", read: true }, { id: "3", read: true },
{ id: "4", read: true }, { id: "4", read: true },
]) ])
expect(client.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true }) expect(client.category.markEntries).toHaveBeenCalledWith({
id: "all",
read: true,
})
}) })
}) })

View File

@@ -1,7 +1,7 @@
import { type PayloadAction, createSlice } from "@reduxjs/toolkit" import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { Constants } from "app/constants" import { Constants } from "@/app/constants"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks" import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "@/app/entries/thunks"
import type { Entry } from "app/types" import type { Entry } from "@/app/types"
export type EntrySourceType = "category" | "feed" | "tag" export type EntrySourceType = "category" | "feed" | "tag"

View File

@@ -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 { 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) => const getEndpoint = (sourceType: EntrySourceType) =>
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
@@ -130,11 +136,12 @@ export const markAllAsReadWithConfirmationIfRequired = createAppAsyncThunk(
const source = state.entries.source const source = state.entries.source
const entriesTimestamp = state.entries.timestamp ?? Date.now() const entriesTimestamp = state.entries.timestamp ?? Date.now()
const markAllAsReadConfirmation = state.user.settings?.markAllAsReadConfirmation const markAllAsReadConfirmation = state.user.settings?.markAllAsReadConfirmation
const markAllAsReadNavigateToNextUnread = state.user.settings?.markAllAsReadNavigateToNextUnread
if (markAllAsReadConfirmation) { if (markAllAsReadConfirmation) {
thunkApi.dispatch(setMarkAllAsReadConfirmationDialogOpen(true)) thunkApi.dispatch(setMarkAllAsReadConfirmationDialogOpen(true))
} else { } else {
thunkApi.dispatch( await thunkApi.dispatch(
markAllEntries({ markAllEntries({
sourceType: source.type, sourceType: source.type,
req: { 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 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 const offset = (header?.bottom ?? 0) + margin
scrollToWithCallback({ scrollToWithCallback({
options: { options: {

View File

@@ -1,6 +1,6 @@
import { redirectToCategory } from "app/redirect/thunks"
import { store } from "app/store"
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { redirectToCategory } from "@/app/redirect/thunks"
import { store } from "@/app/store"
describe("redirects", () => { describe("redirects", () => {
it("redirects to category", async () => { it("redirects to category", async () => {

View File

@@ -1,4 +1,4 @@
import { type PayloadAction, createSlice } from "@reduxjs/toolkit" import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
interface RedirectState { interface RedirectState {
to?: string to?: string

View File

@@ -1,12 +1,14 @@
import { createAppAsyncThunk } from "app/async-thunk" import { createAppAsyncThunk } from "@/app/async-thunk"
import { Constants } from "app/constants" import { Constants } from "@/app/constants"
import { redirectTo } from "app/redirect/slice" import { redirectTo } from "@/app/redirect/slice"
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login"))) export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register"))) 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) => { export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
const { source } = thunkApi.getState().entries const { source } = thunkApi.getState().entries

View File

@@ -1,6 +1,6 @@
import { type PayloadAction, createSlice } from "@reduxjs/toolkit" import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { reloadServerInfos } from "app/server/thunks" import { reloadServerInfos } from "@/app/server/thunks"
import type { ServerInfo } from "app/types" import type { ServerInfo } from "@/app/types"
interface ServerState { interface ServerState {
serverInfos?: ServerInfo serverInfos?: ServerInfo

View File

@@ -1,4 +1,4 @@
import { createAppAsyncThunk } from "app/async-thunk" import { createAppAsyncThunk } from "@/app/async-thunk"
import { client } from "app/client" import { client } from "@/app/client"
export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data)) export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data))

View File

@@ -1,11 +1,11 @@
import { configureStore } from "@reduxjs/toolkit" 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 { 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 = { export const reducers = {
entries: entriesSlice.reducer, entries: entriesSlice.reducer,

View File

@@ -1,12 +1,22 @@
import { type PayloadAction, createSlice } from "@reduxjs/toolkit" import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { markEntry } from "app/entries/thunks" import { loadEntries, markEntry } from "@/app/entries/thunks"
import { redirectTo } from "app/redirect/slice" import { redirectTo } from "@/app/redirect/slice"
import { collapseTreeCategory, reloadTree } from "app/tree/thunks" import { collapseTreeCategory, reloadTree } from "@/app/tree/thunks"
import type { Category } from "app/types" import type { Category, Subscription } from "@/app/types"
import { visitCategoryTree } from "app/utils" 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 { interface TreeState {
rootCategory?: Category rootCategory?: TreeCategory
mobileMenuOpen: boolean mobileMenuOpen: boolean
sidebarVisible: boolean sidebarVisible: boolean
} }
@@ -37,12 +47,27 @@ export const treeSlice = createSlice({
visitCategoryTree(state.rootCategory, c => { visitCategoryTree(state.rootCategory, c => {
for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) { for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) {
f.unread += action.payload.amount f.unread += action.payload.amount
f.hasNewEntries = true
} }
}) })
}, },
}, },
extraReducers: builder => { extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => { 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 state.rootCategory = action.payload
}) })
builder.addCase(collapseTreeCategory.pending, (state, action) => { 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 => { builder.addCase(redirectTo, state => {
state.mobileMenuOpen = false state.mobileMenuOpen = false
}) })

View File

@@ -1,9 +1,10 @@
import { createAppAsyncThunk } from "app/async-thunk" import { createAppAsyncThunk } from "@/app/async-thunk"
import { client } from "app/client" import { client } from "@/app/client"
import { redirectToCategory, redirectToFeed } from "app/redirect/thunks" import { Constants } from "@/app/constants"
import { incrementUnreadCount } from "app/tree/slice" import { redirectToCategory, redirectToFeed } from "@/app/redirect/thunks"
import type { CollapseRequest, Subscription } from "app/types" import { incrementUnreadCount } from "@/app/tree/slice"
import { flattenCategoryTree, visitCategoryTree } from "app/utils" 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)) 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))
} }
) )

View File

@@ -1,8 +1,13 @@
import { configureStore } from "@reduxjs/toolkit" import { configureStore } from "@reduxjs/toolkit"
import { type RootState, reducers } from "app/store" import type { AxiosResponse } from "axios"
import { selectNextUnreadTreeItem } from "app/tree/thunks" import { beforeEach, describe, expect, it, vi } from "vitest"
import type { Category, Subscription } from "app/types" import { client } from "@/app/client"
import { describe, expect, it } from "vitest" 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 => ({ const createCategory = (id: string): Category => ({
id, id,
@@ -117,3 +122,51 @@ describe("selectNextUnreadTreeItem", () => {
expect(store.getState().redirect.to).toBe("/app/feed/3") 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)
})
})

View File

@@ -214,7 +214,6 @@ export interface ServerInfo {
version: string version: string
gitCommit: string gitCommit: string
allowRegistrations: boolean allowRegistrations: boolean
googleAnalyticsCode?: string
smtpEnabled: boolean smtpEnabled: boolean
demoAccountEnabled: boolean demoAccountEnabled: boolean
websocketEnabled: boolean websocketEnabled: boolean
@@ -248,6 +247,7 @@ export interface Settings {
starIconDisplayMode: IconDisplayMode starIconDisplayMode: IconDisplayMode
externalLinkIconDisplayMode: IconDisplayMode externalLinkIconDisplayMode: IconDisplayMode
markAllAsReadConfirmation: boolean markAllAsReadConfirmation: boolean
markAllAsReadNavigateToNextUnread: boolean
customContextMenu: boolean customContextMenu: boolean
mobileFooter: boolean mobileFooter: boolean
unreadCountTitle: boolean unreadCountTitle: boolean

View File

@@ -1,13 +1,14 @@
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { showNotification } from "@mantine/notifications" import { showNotification } from "@mantine/notifications"
import { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit" import { createSlice, isAnyOf, type PayloadAction } from "@reduxjs/toolkit"
import type { LocalSettings, Settings, UserModel, ViewMode } from "app/types" import type { LocalSettings, Settings, UserModel, ViewMode } from "@/app/types"
import { import {
changeCustomContextMenu, changeCustomContextMenu,
changeEntriesToKeepOnTopWhenScrolling, changeEntriesToKeepOnTopWhenScrolling,
changeExternalLinkIconDisplayMode, changeExternalLinkIconDisplayMode,
changeLanguage, changeLanguage,
changeMarkAllAsReadConfirmation, changeMarkAllAsReadConfirmation,
changeMarkAllAsReadNavigateToUnread,
changeMobileFooter, changeMobileFooter,
changePrimaryColor, changePrimaryColor,
changeReadingMode, changeReadingMode,
@@ -114,6 +115,10 @@ export const userSlice = createSlice({
if (!state.settings) return if (!state.settings) return
state.settings.markAllAsReadConfirmation = action.meta.arg 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) => { builder.addCase(changeCustomContextMenu.pending, (state, action) => {
if (!state.settings) return if (!state.settings) return
state.settings.customContextMenu = action.meta.arg state.settings.customContextMenu = action.meta.arg
@@ -149,6 +154,7 @@ export const userSlice = createSlice({
changeStarIconDisplayMode.fulfilled, changeStarIconDisplayMode.fulfilled,
changeExternalLinkIconDisplayMode.fulfilled, changeExternalLinkIconDisplayMode.fulfilled,
changeMarkAllAsReadConfirmation.fulfilled, changeMarkAllAsReadConfirmation.fulfilled,
changeMarkAllAsReadNavigateToUnread.fulfilled,
changeCustomContextMenu.fulfilled, changeCustomContextMenu.fulfilled,
changeMobileFooter.fulfilled, changeMobileFooter.fulfilled,
changeUnreadCountTitle.fulfilled, changeUnreadCountTitle.fulfilled,

View File

@@ -1,7 +1,7 @@
import { createAppAsyncThunk } from "app/async-thunk" import { createAppAsyncThunk } from "@/app/async-thunk"
import { client } from "app/client" import { client } from "@/app/client"
import { reloadEntries } from "app/entries/thunks" import { reloadEntries } from "@/app/entries/thunks"
import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types" 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)) 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) => { export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user const { settings } = thunkApi.getState().user
if (!settings) return if (!settings) return

View File

@@ -1,9 +1,10 @@
import { throttle } from "throttle-debounce" import { throttle } from "throttle-debounce"
import type { TreeCategory } from "@/app/tree/slice"
import type { Category } from "./types" import type { Category } from "./types"
export function visitCategoryTree( export function visitCategoryTree(
category: Category, category: TreeCategory,
visitor: (category: Category) => void, visitor: (category: TreeCategory) => void,
options?: { options?: {
childrenFirst?: boolean childrenFirst?: boolean
} }
@@ -19,13 +20,13 @@ export function visitCategoryTree(
if (childrenFirst) visitor(category) if (childrenFirst) visitor(category)
} }
export function flattenCategoryTree(category: Category): Category[] { export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
const categories: Category[] = [] const categories: Category[] = []
visitCategoryTree(category, c => categories.push(c)) visitCategoryTree(category, c => categories.push(c))
return categories return categories
} }
export function categoryUnreadCount(category?: Category): number { export function categoryUnreadCount(category?: TreeCategory): number {
if (!category) return 0 if (!category) return 0
return flattenCategoryTree(category) return flattenCategoryTree(category)
@@ -34,6 +35,14 @@ export function categoryUnreadCount(category?: Category): number {
.reduce((total, current) => total + current, 0) .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 }) => { export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => {
const placeholderWidth = width && Math.min(width, maxWidth) const placeholderWidth = width && Math.min(width, maxWidth)
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height

View File

@@ -1,8 +1,8 @@
import type { I18nContext } from "@lingui/react" import type { I18nContext } from "@lingui/react"
import { MantineProvider } from "@mantine/core" import { MantineProvider } from "@mantine/core"
import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { useActionButton } from "hooks/useActionButton"
import { describe, expect, it, vi } from "vitest" import { describe, expect, it, vi } from "vitest"
import { useActionButton } from "@/hooks/useActionButton"
import { ActionButton } from "./ActionButton" import { ActionButton } from "./ActionButton"
vi.mock(import("@lingui/react"), () => ({ vi.mock(import("@lingui/react"), () => ({
@@ -10,7 +10,7 @@ vi.mock(import("@lingui/react"), () => ({
_: msg => msg, _: msg => msg,
} as I18nContext), } as I18nContext),
})) }))
vi.mock(import("hooks/useActionButton")) vi.mock(import("@/hooks/useActionButton"))
const label = "Test Label" const label = "Test Label"
const icon = "Test Icon" const icon = "Test Icon"
@@ -18,7 +18,9 @@ describe("ActionButton", () => {
it("renders Button with label on desktop", () => { it("renders Button with label on desktop", () => {
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 }) 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(label)).toBeInTheDocument()
expect(screen.getByText(icon)).toBeInTheDocument() expect(screen.getByText(icon)).toBeInTheDocument()
}) })
@@ -26,7 +28,9 @@ describe("ActionButton", () => {
it("renders ActionIcon with tooltip on mobile", async () => { it("renders ActionIcon with tooltip on mobile", async () => {
vi.mocked(useActionButton).mockReturnValue({ mobile: true, spacing: 0 }) 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.queryByText(label)).not.toBeInTheDocument()
expect(screen.getByText(icon)).toBeInTheDocument() expect(screen.getByText(icon)).toBeInTheDocument()
@@ -39,7 +43,9 @@ describe("ActionButton", () => {
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 }) vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 })
const clickListener = vi.fn() 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")) fireEvent.click(screen.getByRole("button"))
expect(clickListener).toHaveBeenCalled() expect(clickListener).toHaveBeenCalled()

View File

@@ -2,9 +2,9 @@ import type { MessageDescriptor } from "@lingui/core"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { ActionIcon, Box, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core" import { ActionIcon, Box, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon" import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
import { Constants } from "app/constants" import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
import { useActionButton } from "hooks/useActionButton" import { Constants } from "@/app/constants"
import { type MouseEventHandler, type ReactNode, forwardRef } from "react" import { useActionButton } from "@/hooks/useActionButton"
interface ActionButtonProps { interface ActionButtonProps {
icon: ReactNode icon: ReactNode
@@ -29,7 +29,7 @@ export const ActionButton = forwardRef<HTMLDivElement, ActionButtonProps>((props
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop) const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
return ( return (
<Box ref={ref}> <Box ref={ref} className="cf-action-button">
{iconOnly && ( {iconOnly && (
<Tooltip label={label} openDelay={Constants.tooltip.delay}> <Tooltip label={label} openDelay={Constants.tooltip.delay}>
<ActionIcon <ActionIcon

View File

@@ -10,7 +10,7 @@ export interface ErrorsAlertProps {
messages: string[] messages: string[]
} }
export function Alert(props: ErrorsAlertProps) { export function Alert(props: Readonly<ErrorsAlertProps>) {
let title: React.ReactNode let title: React.ReactNode
let color: string let color: string
let icon: React.ReactNode let icon: React.ReactNode

View File

@@ -1,9 +1,9 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Dialog, Text } from "@mantine/core" 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 { 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 sha256Hex = async (input: string | undefined) => {
const data = new TextEncoder().encode(input) const data = new TextEncoder().encode(input)

View File

@@ -1,4 +1,4 @@
export const DisablePullToRefresh = () => { export const DisablePullToRefresh = () => {
import("./DisablePullToRefresh.css") import("./DisablePullToRefresh.css")
return <></> return null
} }

View File

@@ -1,5 +1,5 @@
import { ErrorPage } from "pages/ErrorPage"
import React, { type ReactNode } from "react" import React, { type ReactNode } from "react"
import { ErrorPage } from "@/pages/ErrorPage"
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
children?: ReactNode children?: ReactNode

View File

@@ -1,7 +1,7 @@
import { Box, Center } from "@mantine/core" import { Box, Center } from "@mantine/core"
import { useState } from "react" import { useState } from "react"
import { TbPhoto } from "react-icons/tb" import { TbPhoto } from "react-icons/tb"
import { tss } from "tss" import { tss } from "@/tss"
interface ImageWithPlaceholderWhileLoadingProps { interface ImageWithPlaceholderWhileLoadingProps {
src: string src: string
@@ -44,7 +44,7 @@ export function ImageWithPlaceholderWhileLoading({
title, title,
width, width,
style, style,
}: ImageWithPlaceholderWhileLoadingProps) { }: Readonly<ImageWithPlaceholderWhileLoadingProps>) {
const { classes } = useStyles({ const { classes } = useStyles({
placeholderWidth, placeholderWidth,
placeholderHeight, placeholderHeight,

View File

@@ -1,7 +1,7 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core" import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
import { useOs } from "@mantine/hooks" import { useOs } from "@mantine/hooks"
import { Constants } from "app/constants" import { Constants } from "@/app/constants"
export function KeyboardShortcutsHelp() { export function KeyboardShortcutsHelp() {
const isMacOS = useOs() === "macos" const isMacOS = useOs() === "macos"

View File

@@ -1,10 +1,10 @@
import { Image } from "@mantine/core" import { Image } from "@mantine/core"
import logo from "assets/logo.svg" import logo from "@/assets/logo.svg"
export interface LogoProps { export interface LogoProps {
size: number size: number
} }
export function Logo(props: LogoProps) { export function Logo(props: Readonly<LogoProps>) {
return <Image src={logo} w={props.size} /> return <Image src={logo} w={props.size} />
} }

View File

@@ -1,9 +1,11 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core" 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 { 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() { export function MarkAllAsReadConfirmationDialog() {
const [threshold, setThreshold] = useState(0) const [threshold, setThreshold] = useState(0)
@@ -11,10 +13,12 @@ export function MarkAllAsReadConfirmationDialog() {
const source = useAppSelector(state => state.entries.source) const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel) const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now() const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const markAllAsReadNavigateToNextUnread = useAppSelector(state => state.user.settings?.markAllAsReadNavigateToNextUnread)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const onConfirm = () => { const onConfirm = async () => {
dispatch( dispatch(setMarkAllAsReadConfirmationDialogOpen(false))
await dispatch(
markAllEntries({ markAllEntries({
sourceType: source.type, sourceType: source.type,
req: { 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 ( return (

View File

@@ -1,10 +1,14 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Tooltip } from "@mantine/core" import { Tooltip } from "@mantine/core"
import { Constants } from "app/constants"
import dayjs from "dayjs" 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) const now = useNow(60 * 1000)
if (!props.date) return <Trans>N/A</Trans> if (!props.date) return <Trans>N/A</Trans>

View File

@@ -1,11 +1,11 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core" import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import type { AdminSaveUserRequest, UserModel } from "app/types"
import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb" import { TbDeviceFloppy } from "react-icons/tb"
import { client, errorToStrings } from "@/app/client"
import type { AdminSaveUserRequest, UserModel } from "@/app/types"
import { Alert } from "@/components/Alert"
interface UserEditProps { interface UserEditProps {
user?: UserModel user?: UserModel
@@ -13,7 +13,7 @@ interface UserEditProps {
onSave: () => void onSave: () => void
} }
export function UserEdit(props: UserEditProps) { export function UserEdit(props: Readonly<UserEditProps>) {
const form = useForm<AdminSaveUserRequest>({ const form = useForm<AdminSaveUserRequest>({
initialValues: props.user ?? { initialValues: props.user ?? {
name: "", name: "",

View File

@@ -1,16 +1,17 @@
import { Input, Textarea } from "@mantine/core" import { Input, Textarea } from "@mantine/core"
import RichCodeEditor from "components/code/RichCodeEditor"
import { useMobile } from "hooks/useMobile"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import RichCodeEditor from "@/components/code/RichCodeEditor"
import { useMobile } from "@/hooks/useMobile"
interface CodeEditorProps { interface CodeEditorProps {
label?: ReactNode
description?: ReactNode description?: ReactNode
language: "css" | "javascript" language: "css" | "javascript"
value?: string value?: string
onChange: (value: string | undefined) => void onChange: (value: string | undefined) => void
} }
export function CodeEditor(props: CodeEditorProps) { export function CodeEditor(props: Readonly<CodeEditorProps>) {
const mobile = useMobile() const mobile = useMobile()
return mobile ? ( return mobile ? (
@@ -19,7 +20,8 @@ export function CodeEditor(props: CodeEditorProps) {
autosize autosize
minRows={4} minRows={4}
maxRows={15} maxRows={15}
label={props.description} label={props.label}
description={props.description}
styles={{ styles={{
input: { input: {
fontFamily: "monospace", fontFamily: "monospace",
@@ -29,7 +31,7 @@ export function CodeEditor(props: CodeEditorProps) {
onChange={e => props.onChange(e.currentTarget.value)} onChange={e => props.onChange(e.currentTarget.value)}
/> />
) : ( ) : (
<Input.Wrapper label={props.description}> <Input.Wrapper label={props.label} description={props.description}>
<RichCodeEditor height="30vh" language={props.language} value={props.value} onChange={props.onChange} /> <RichCodeEditor height="30vh" language={props.language} value={props.value} onChange={props.onChange} />
</Input.Wrapper> </Input.Wrapper>
) )

View File

@@ -1,6 +1,6 @@
import { Loader } from "components/Loader"
import { useColorScheme } from "hooks/useColorScheme"
import { useAsync } from "react-async-hook" import { useAsync } from "react-async-hook"
import { Loader } from "@/components/Loader"
import { useColorScheme } from "@/hooks/useColorScheme"
const init = async () => { const init = async () => {
window.MonacoEnvironment = { window.MonacoEnvironment = {
@@ -30,7 +30,7 @@ interface RichCodeEditorProps {
onChange: (value: string | undefined) => void onChange: (value: string | undefined) => void
} }
function RichCodeEditor(props: RichCodeEditorProps) { function RichCodeEditor(props: Readonly<RichCodeEditorProps>) {
const colorScheme = useColorScheme() const colorScheme = useColorScheme()
const editorTheme = colorScheme === "dark" ? "vs-dark" : "light" const editorTheme = colorScheme === "dark" ? "vs-dark" : "light"

View File

@@ -1,6 +1,6 @@
import { TypographyStylesProvider } from "@mantine/core" import { Typography } from "@mantine/core"
import type { ReactNode } from "react" 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. * 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 }) => { export const BasicHtmlStyles = (props: { children: ReactNode }) => {
const { classes } = useStyles() const { classes } = useStyles()
return <TypographyStylesProvider className={classes.content}>{props.children}</TypographyStylesProvider> return <Typography className={classes.content}>{props.children}</Typography>
} }

View File

@@ -1,7 +1,7 @@
import { MantineProvider } from "@mantine/core" import { MantineProvider } from "@mantine/core"
import { render } from "@testing-library/react" import { render } from "@testing-library/react"
import { Content } from "components/content/Content"
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { Content } from "@/components/content/Content"
describe("Content component", () => { describe("Content component", () => {
it("renders basic content", () => { it("renders basic content", () => {

View File

@@ -1,13 +1,13 @@
import { Box, Mark } from "@mantine/core" 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 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 React from "react"
import styleToObject from "style-to-object" 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 { export interface ContentProps {
content: string content: string

View File

@@ -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: { export function Enclosure(
enclosureType: string props: Readonly<{
enclosureUrl: string enclosureType: string
}) { enclosureUrl: string
}>
) {
const hasVideo = props.enclosureType.startsWith("video") const hasVideo = props.enclosureType.startsWith("video")
const hasAudio = props.enclosureType.startsWith("audio") const hasAudio = props.enclosureType.startsWith("audio")
const hasImage = props.enclosureType.startsWith("image") const hasImage = props.enclosureType.startsWith("image")

View File

@@ -1,8 +1,12 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { openModal } from "@mantine/modals" import { openModal } from "@mantine/modals"
import { Constants } from "app/constants" import { useEffect } from "react"
import type { ExpendableEntry } from "app/entries/slice" 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 { import {
loadMoreEntries, loadMoreEntries,
markAllAsReadWithConfirmationIfRequired, markAllAsReadWithConfirmationIfRequired,
@@ -12,19 +16,15 @@ import {
selectNextEntry, selectNextEntry,
selectPreviousEntry, selectPreviousEntry,
starEntry, starEntry,
} from "app/entries/thunks" } from "@/app/entries/thunks"
import { redirectToRootCategory } from "app/redirect/thunks" import { redirectToRootCategory } from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "@/app/store"
import { toggleSidebar } from "app/tree/slice" import { toggleSidebar } from "@/app/tree/slice"
import { selectNextUnreadTreeItem } from "app/tree/thunks" import { selectNextUnreadTreeItem } from "@/app/tree/thunks"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "@/components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader" import { Loader } from "@/components/Loader"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "@/hooks/useBrowserExtension"
import { useMousetrap } from "hooks/useMousetrap" 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"
import { FeedEntry } from "./FeedEntry" import { FeedEntry } from "./FeedEntry"
export function FeedEntries() { export function FeedEntries() {
@@ -287,8 +287,7 @@ export function FeedEntries() {
return ( return (
<InfiniteScroll <InfiniteScroll
id="entries" className={`cf-entries cf-view-mode-${viewMode}`}
className={`view-mode-${viewMode}`}
initialLoad={false} initialLoad={false}
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))} loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
hasMore={hasMore} hasMore={hasMore}

View File

@@ -1,13 +1,13 @@
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core" import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
import { Constants } from "app/constants"
import { 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 type React from "react"
import { useSwipeable } from "react-swipeable" 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 { FeedEntryBody } from "./FeedEntryBody"
import { FeedEntryContextMenu } from "./FeedEntryContextMenu" import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
import { FeedEntryFooter } from "./FeedEntryFooter" 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 viewMode = useAppSelector(state => state.user.localSettings.viewMode)
const fontSizePercentage = useAppSelector(state => state.user.localSettings.fontSizePercentage) const fontSizePercentage = useAppSelector(state => state.user.localSettings.fontSizePercentage)
const { classes, cx } = useStyles({ const { classes, cx } = useStyles({
@@ -184,10 +184,10 @@ export function FeedEntry(props: FeedEntryProps) {
</a> </a>
{props.expanded && ( {props.expanded && (
<Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}> <Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
<Box className={classes.body}> <Box className={`${classes.body} cf-content`}>
<FeedEntryBody entry={props.entry} /> <FeedEntryBody entry={props.entry} />
</Box> </Box>
<Divider variant="dashed" my={paddingY} /> <Divider variant="dashed" my={paddingY} className="cf-footer-divider" />
<FeedEntryFooter entry={props.entry} /> <FeedEntryFooter entry={props.entry} />
</Box> </Box>
)} )}

View File

@@ -1,6 +1,6 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useAppSelector } from "app/store" import { useAppSelector } from "@/app/store"
import type { Entry } from "app/types" import type { Entry } from "@/app/types"
import { Content } from "./Content" import { Content } from "./Content"
import { Enclosure } from "./Enclosure" import { Enclosure } from "./Enclosure"
import { Media } from "./Media" import { Media } from "./Media"
@@ -9,7 +9,7 @@ export interface FeedEntryBodyProps {
entry: Entry entry: Entry
} }
export function FeedEntryBody(props: FeedEntryBodyProps) { export function FeedEntryBody(props: Readonly<FeedEntryBodyProps>) {
const search = useAppSelector(state => state.entries.search) const search = useAppSelector(state => state.entries.search)
return ( return (
<Box> <Box>

View File

@@ -1,16 +1,16 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Group } from "@mantine/core" 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 { Item, Menu, Separator } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbRss, TbStar, TbStarOff } from "react-icons/tb" 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 { interface FeedEntryContextMenuProps {
entry: Entry 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 colorScheme = useColorScheme()
const { classes } = useStyles() const { classes } = useStyles()
const sourceType = useAppSelector(state => state.entries.source.type) const sourceType = useAppSelector(state => state.entries.source.type)

View File

@@ -1,20 +1,20 @@
import { msg } from "@lingui/core/macro" import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { Group, Indicator, Popover, TagsInput } from "@mantine/core" import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
import { 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 { 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" import { ShareButtons } from "./ShareButtons"
interface FeedEntryFooterProps { interface FeedEntryFooterProps {
entry: Entry entry: Entry
} }
export function FeedEntryFooter(props: FeedEntryFooterProps) { export function FeedEntryFooter(props: Readonly<FeedEntryFooterProps>) {
const tags = useAppSelector(state => state.user.tags) const tags = useAppSelector(state => state.user.tags)
const mobile = useMobile() const mobile = useMobile()
const { spacing } = useActionButton() const { spacing } = useActionButton()
@@ -37,7 +37,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
) )
return ( return (
<Group justify="space-between"> <Group justify="space-between" className="cf-footer">
<Group gap={spacing}> <Group gap={spacing}>
{props.entry.markable && ( {props.entry.markable && (
<ActionButton <ActionButton

View File

@@ -1,11 +1,11 @@
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { ImageWithPlaceholderWhileLoading } from "@/components/ImageWithPlaceholderWhileLoading"
export interface FeedFaviconProps { export interface FeedFaviconProps {
url: string url: string
size?: number size?: number
} }
export function FeedFavicon({ url, size = 18 }: FeedFaviconProps) { export function FeedFavicon({ url, size = 18 }: Readonly<FeedFaviconProps>) {
return ( return (
<ImageWithPlaceholderWhileLoading <ImageWithPlaceholderWhileLoading
src={url} src={url}

View File

@@ -1,8 +1,8 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "@/app/constants"
import { calculatePlaceholderSize } from "app/utils" import { calculatePlaceholderSize } from "@/app/utils"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { BasicHtmlStyles } from "@/components/content/BasicHtmlStyles"
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles" import { ImageWithPlaceholderWhileLoading } from "@/components/ImageWithPlaceholderWhileLoading"
import { Content } from "./Content" import { Content } from "./Content"
export interface MediaProps { export interface MediaProps {
@@ -12,7 +12,7 @@ export interface MediaProps {
description?: string description?: string
} }
export function Media(props: MediaProps) { export function Media(props: Readonly<MediaProps>) {
const width = props.thumbnailWidth const width = props.thumbnailWidth
const height = props.thumbnailHeight const height = props.thumbnailHeight
const placeholderSize = calculatePlaceholderSize({ const placeholderSize = calculatePlaceholderSize({

View File

@@ -1,13 +1,13 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core" import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core"
import { Constants } from "app/constants"
import { 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 type { IconType } from "react-icons"
import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb" import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb"
import { tss } from "tss" import { 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}` 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({ const { classes } = useStyles({
color, 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 = () => { const onClick = () => {
window.open(url, "", "menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=800,height=600") 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} /> return <ShareButton icon={icon} color={color} onClick={onClick} />
} }
function CopyUrlButton({ url }: { url: string }) { function CopyUrlButton({
url,
}: Readonly<{
url: string
}>) {
return ( return (
<CopyButton value={url}> <CopyButton value={url}>
{({ copied, copy }) => <ShareButton icon={copied ? TbCheck : TbCopy} color="#000" onClick={copy} />} {({ 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 mobile = useMobile()
const { isBrowserExtensionPopup } = useBrowserExtension() const { isBrowserExtensionPopup } = useBrowserExtension()
const onClick = () => { 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 sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const enabledSharingSites = (Object.keys(Constants.sharing) as Array<keyof SharingSettings>).filter(site => sharingSettings?.[site]) const enabledSharingSites = (Object.keys(Constants.sharing) as Array<keyof SharingSettings>).filter(site => sharingSettings?.[site])
const url = encodeURIComponent(props.url) const url = encodeURIComponent(props.url)

View File

@@ -3,14 +3,14 @@ import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Button, Group, Stack, TextInput } from "@mantine/core" import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import { 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 { useAsyncCallback } from "react-async-hook"
import { TbFolderPlus } from "react-icons/tb" 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" import { CategorySelect } from "./CategorySelect"
export function AddCategory() { export function AddCategory() {

View File

@@ -2,10 +2,10 @@ import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { Select, type SelectProps } from "@mantine/core" import { Select, type SelectProps } from "@mantine/core"
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types" import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { Constants } from "app/constants" import { Constants } from "@/app/constants"
import { useAppSelector } from "app/store" import { useAppSelector } from "@/app/store"
import type { Category } from "app/types" import type { Category } from "@/app/types"
import { flattenCategoryTree } from "app/utils" import { flattenCategoryTree } from "@/app/utils"
type CategorySelectProps = Partial<SelectProps> & { type CategorySelectProps = Partial<SelectProps> & {
withAll?: boolean withAll?: boolean

View File

@@ -3,13 +3,13 @@ import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Button, FileInput, Group, Stack } from "@mantine/core" import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
import { isNotEmpty, useForm } from "@mantine/form" import { isNotEmpty, useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import { 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 { useAsyncCallback } from "react-async-hook"
import { TbFileImport } from "react-icons/tb" 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() { export function ImportOpml() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()

View File

@@ -1,16 +1,16 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core" import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" 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 { useState } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbRss } from "react-icons/tb" 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" import { CategorySelect } from "./CategorySelect"
export function Subscribe() { export function Subscribe() {
@@ -39,9 +39,8 @@ export function Subscribe() {
}, },
}) })
const subscribe = useAsyncCallback(client.feed.subscribe, { const subscribe = useAsyncCallback(client.feed.subscribe, {
onSuccess: async sub => { onSuccess: sub => {
await dispatch(reloadTree()) dispatch(reloadTree()).then(() => dispatch(redirectToFeed(sub.data)))
dispatch(redirectToFeed(sub.data))
}, },
}) })

View File

@@ -1,11 +1,11 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import type { Entry } from "app/types" import type { Entry } from "@/app/types"
import { RelativeDate } from "components/RelativeDate" import { FeedFavicon } from "@/components/content/FeedFavicon"
import { FeedFavicon } from "components/content/FeedFavicon" import { OpenExternalLink } from "@/components/content/header/OpenExternalLink"
import { OpenExternalLink } from "components/content/header/OpenExternalLink" import { Star } from "@/components/content/header/Star"
import { Star } from "components/content/header/Star" import { RelativeDate } from "@/components/RelativeDate"
import { OnDesktop } from "components/responsive/OnDesktop" import { OnDesktop } from "@/components/responsive/OnDesktop"
import { tss } from "tss" import { tss } from "@/tss"
import { FeedEntryTitle } from "./FeedEntryTitle" import { FeedEntryTitle } from "./FeedEntryTitle"
export interface FeedEntryHeaderProps { export interface FeedEntryHeaderProps {
@@ -43,7 +43,7 @@ const useStyles = tss
}, },
})) }))
export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) { export function FeedEntryCompactHeader(props: Readonly<FeedEntryHeaderProps>) {
const { classes } = useStyles({ const { classes } = useStyles({
read: props.entry.read, read: props.entry.read,
}) })

View File

@@ -1,10 +1,10 @@
import { Box, Flex, Space } from "@mantine/core" import { Box, Flex, Space } from "@mantine/core"
import type { Entry } from "app/types" import type { Entry } from "@/app/types"
import { RelativeDate } from "components/RelativeDate" import { FeedFavicon } from "@/components/content/FeedFavicon"
import { FeedFavicon } from "components/content/FeedFavicon" import { OpenExternalLink } from "@/components/content/header/OpenExternalLink"
import { OpenExternalLink } from "components/content/header/OpenExternalLink" import { Star } from "@/components/content/header/Star"
import { Star } from "components/content/header/Star" import { RelativeDate } from "@/components/RelativeDate"
import { tss } from "tss" import { tss } from "@/tss"
import { FeedEntryTitle } from "./FeedEntryTitle" import { FeedEntryTitle } from "./FeedEntryTitle"
export interface FeedEntryHeaderProps { export interface FeedEntryHeaderProps {
@@ -24,13 +24,13 @@ const useStyles = tss
}, },
})) }))
export function FeedEntryHeader(props: FeedEntryHeaderProps) { export function FeedEntryHeader(props: Readonly<FeedEntryHeaderProps>) {
const { classes } = useStyles({ const { classes } = useStyles({
read: props.entry.read, read: props.entry.read,
}) })
return ( return (
<Box> <Box className="cf-header">
<Flex align="flex-start" justify="space-between"> <Flex align="flex-start" justify="space-between" className="cf-header-title">
<Flex align="flex-start" className={classes.main}> <Flex align="flex-start" className={classes.main}>
{props.showStarIcon && ( {props.showStarIcon && (
<Box ml={-5}> <Box ml={-5}>
@@ -41,7 +41,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
</Flex> </Flex>
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />} {props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
</Flex> </Flex>
<Flex align="center"> <Flex align="center" className="cf-header-subtitle">
<FeedFavicon url={props.entry.iconUrl} /> <FeedFavicon url={props.entry.iconUrl} />
<Space w={6} /> <Space w={6} />
<Box c="dimmed"> <Box c="dimmed">
@@ -51,7 +51,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
</Box> </Box>
</Flex> </Flex>
{props.expanded && ( {props.expanded && (
<Box c="dimmed"> <Box className="cf-header-details">
{props.entry.author && <span>by {props.entry.author}</span>} {props.entry.author && <span>by {props.entry.author}</span>}
{props.entry.author && props.entry.categories && <span>&nbsp;·&nbsp;</span>} {props.entry.author && props.entry.categories && <span>&nbsp;·&nbsp;</span>}
{props.entry.categories && <span>{props.entry.categories}</span>} {props.entry.categories && <span>{props.entry.categories}</span>}

View File

@@ -1,12 +1,12 @@
import { Highlight } from "@mantine/core" import { Highlight } from "@mantine/core"
import { useAppSelector } from "app/store" import { useAppSelector } from "@/app/store"
import type { Entry } from "app/types" import type { Entry } from "@/app/types"
export interface FeedEntryTitleProps { export interface FeedEntryTitleProps {
entry: Entry entry: Entry
} }
export function FeedEntryTitle(props: FeedEntryTitleProps) { export function FeedEntryTitle(props: Readonly<FeedEntryTitleProps>) {
const search = useAppSelector(state => state.entries.search) const search = useAppSelector(state => state.entries.search)
const keywords = search?.split(" ") const keywords = search?.split(" ")
return ( return (

View File

@@ -1,12 +1,16 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { ActionIcon, Anchor, Tooltip } from "@mantine/core" 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 { 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 dispatch = useAppDispatch()
const onClick = (e: React.MouseEvent) => { const onClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()

View File

@@ -1,12 +1,16 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { ActionIcon, Tooltip } from "@mantine/core" 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 { 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 dispatch = useAppDispatch()
const onClick = (e: React.MouseEvent) => { const onClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()

View File

@@ -2,14 +2,6 @@ import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core" import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { 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 { useEffect } from "react"
import { import {
TbArrowDown, TbArrowDown,
@@ -25,6 +17,14 @@ import {
TbSortDescending, TbSortDescending,
TbUser, TbUser,
} from "react-icons/tb" } 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" import { ProfileMenu } from "./ProfileMenu"
function HeaderDivider() { function HeaderDivider() {
@@ -42,11 +42,14 @@ function HeaderToolbar(props: { children: React.ReactNode }) {
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
}} }}
className="cf-toolbar"
> >
{props.children} {props.children}
</Box> </Box>
) : ( ) : (
<Group gap={spacing}>{props.children}</Group> <Group gap={spacing} className="cf-toolbar">
{props.children}
</Group>
) )
} }
@@ -75,7 +78,7 @@ export function Header() {
if (!settings) return <Loader /> if (!settings) return <Loader />
return ( return (
<Center> <Center className="cf-toolbar-wrapper">
<HeaderToolbar> <HeaderToolbar>
<ActionButton <ActionButton
icon={<TbArrowUp size={iconSize} />} icon={<TbArrowUp size={iconSize} />}

View File

@@ -11,14 +11,7 @@ import {
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core" } from "@mantine/core"
import { showNotification } from "@mantine/notifications" 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 dayjs from "dayjs"
import { useNow } from "hooks/useNow"
import { type ReactNode, useState } from "react" import { type ReactNode, useState } from "react"
import { import {
TbChartLine, TbChartLine,
@@ -36,6 +29,13 @@ import {
TbUsers, TbUsers,
TbWorldDownload, TbWorldDownload,
} from "react-icons/tb" } 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 { interface ProfileMenuProps {
control: React.ReactElement 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 [opened, setOpened] = useState(false)
const now = useNow() const now = useNow()
const profile = useAppSelector(state => state.user.profile) const profile = useAppSelector(state => state.user.profile)
@@ -145,7 +145,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
color: "green", color: "green",
autoClose: 1000, autoClose: 1000,
}) })
} catch (_) { } catch {
showNotification({ showNotification({
message: <Trans>Force fetching feeds is not yet available.</Trans>, message: <Trans>Force fetching feeds is not yet available.</Trans>,
color: "red", color: "red",

View File

@@ -1,10 +1,10 @@
import { NumberFormatter } from "@mantine/core" import { NumberFormatter } from "@mantine/core"
import type { MetricGauge } from "app/types" import type { MetricGauge } from "@/app/types"
interface MeterProps { interface GaugeProps {
gauge: MetricGauge gauge: MetricGauge
} }
export function Gauge(props: MeterProps) { export function Gauge(props: Readonly<GaugeProps>) {
return <NumberFormatter value={props.gauge.value} thousandSeparator /> return <NumberFormatter value={props.gauge.value} thousandSeparator />
} }

View File

@@ -1,11 +1,11 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import type { MetricMeter } from "app/types" import type { MetricMeter } from "@/app/types"
interface MeterProps { interface MeterProps {
meter: MetricMeter meter: MetricMeter
} }
export function Meter(props: MeterProps) { export function Meter(props: Readonly<MeterProps>) {
return ( return (
<Box> <Box>
<Box>Mean: {props.meter.mean_rate.toFixed(2)}</Box> <Box>Mean: {props.meter.mean_rate.toFixed(2)}</Box>

View File

@@ -7,7 +7,7 @@ interface MetricAccordionItemProps {
children: React.ReactNode children: React.ReactNode
} }
export function MetricAccordionItem({ metricKey, name, headerValue, children }: MetricAccordionItemProps) { export function MetricAccordionItem({ metricKey, name, headerValue, children }: Readonly<MetricAccordionItemProps>) {
return ( return (
<Accordion.Item value={metricKey} key={metricKey}> <Accordion.Item value={metricKey} key={metricKey}>
<Accordion.Control> <Accordion.Control>

View File

@@ -1,11 +1,11 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import type { MetricTimer } from "app/types" import type { MetricTimer } from "@/app/types"
interface MetricTimerProps { interface MetricTimerProps {
timer: MetricTimer timer: MetricTimer
} }
export function Timer(props: MetricTimerProps) { export function Timer(props: Readonly<MetricTimerProps>) {
return ( return (
<Box> <Box>
<Box>Mean: {props.timer.mean_rate.toFixed(2)}</Box> <Box>Mean: {props.timer.mean_rate.toFixed(2)}</Box>

View File

@@ -1,8 +1,12 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useMobile } from "hooks/useMobile"
import type React from "react" 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() const mobile = useMobile()
return <Box>{!mobile && props.children}</Box> return <Box>{!mobile && props.children}</Box>
} }

View File

@@ -1,8 +1,12 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useMobile } from "hooks/useMobile"
import type React from "react" 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() const mobile = useMobile()
return <Box>{mobile && props.children}</Box> return <Box>{mobile && props.children}</Box>
} }

View File

@@ -1,14 +1,15 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Button, Group, Stack } from "@mantine/core" import { Anchor, Box, Button, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
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 { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb" 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 { interface FormData {
customCss: string customCss: string
@@ -57,13 +58,23 @@ export function CustomCodeSettings() {
<form onSubmit={form.onSubmit(saveCustomCode.execute)}> <form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Stack> <Stack>
<CodeEditor <CodeEditor
description={<Trans>Custom CSS rules that will be applied</Trans>} label={<Trans>Custom CSS rules that will be applied</Trans>}
description={
<Anchor
href={Constants.customCssDocumentationUrl}
target="_blank"
rel="noreferrer"
style={{ fontSize: "inherit" }}
>
<Trans>Link to the documentation</Trans>
</Anchor>
}
language="css" language="css"
{...form.getInputProps("customCss")} {...form.getInputProps("customCss")}
/> />
<CodeEditor <CodeEditor
description={<Trans>Custom JS code that will be executed on page load</Trans>} label={<Trans>Custom JS code that will be executed on page load</Trans>}
language="javascript" language="javascript"
{...form.getInputProps("customJs")} {...form.getInputProps("customJs")}
/> />

View File

@@ -3,15 +3,17 @@ import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Divider, Group, NumberInput, Radio, Select, type SelectProps, SimpleGrid, Stack, Switch } from "@mantine/core" 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 type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { Constants } from "app/constants" import type { ReactNode } from "react"
import { useAppDispatch, useAppSelector } from "app/store" import { Constants } from "@/app/constants"
import type { IconDisplayMode, ScrollMode, SharingSettings } from "app/types" import { useAppDispatch, useAppSelector } from "@/app/store"
import type { IconDisplayMode, ScrollMode, SharingSettings } from "@/app/types"
import { import {
changeCustomContextMenu, changeCustomContextMenu,
changeEntriesToKeepOnTopWhenScrolling, changeEntriesToKeepOnTopWhenScrolling,
changeExternalLinkIconDisplayMode, changeExternalLinkIconDisplayMode,
changeLanguage, changeLanguage,
changeMarkAllAsReadConfirmation, changeMarkAllAsReadConfirmation,
changeMarkAllAsReadNavigateToUnread,
changeMobileFooter, changeMobileFooter,
changePrimaryColor, changePrimaryColor,
changeScrollMarks, changeScrollMarks,
@@ -22,9 +24,8 @@ import {
changeStarIconDisplayMode, changeStarIconDisplayMode,
changeUnreadCountFavicon, changeUnreadCountFavicon,
changeUnreadCountTitle, changeUnreadCountTitle,
} from "app/user/thunks" } from "@/app/user/thunks"
import { locales } from "i18n" import { locales } from "@/i18n"
import type { ReactNode } from "react"
export function DisplaySettings() { export function DisplaySettings() {
const language = useAppSelector(state => state.user.settings?.language) const language = useAppSelector(state => state.user.settings?.language)
@@ -36,6 +37,7 @@ export function DisplaySettings() {
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode) const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode) const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation) 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 customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter) const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle) const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
@@ -127,6 +129,12 @@ export function DisplaySettings() {
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))} 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 <Switch
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>} label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
checked={mobileFooter} checked={mobileFooter}

View File

@@ -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 { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals" import { openConfirmModal } from "@mantine/modals"
import { client, errorToStrings } from "app/client"
import { 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 { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy, TbTrash } from "react-icons/tb" 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 { interface FormData extends ProfileModificationRequest {
newPasswordConfirmation?: string newPasswordConfirmation?: string
@@ -52,7 +52,9 @@ export function ProfileSettings() {
), ),
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> }, labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onConfirm: async () => await deleteProfile.execute(), onConfirm: () => {
deleteProfile.execute()
},
}) })
useEffect(() => { useEffect(() => {

View File

@@ -1,6 +1,8 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Stack } from "@mantine/core" 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 { import {
redirectToCategory, redirectToCategory,
redirectToCategoryDetails, redirectToCategoryDetails,
@@ -8,15 +10,14 @@ import {
redirectToFeedDetails, redirectToFeedDetails,
redirectToTag, redirectToTag,
redirectToTagDetails, redirectToTagDetails,
} from "app/redirect/thunks" } from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "@/app/store"
import { collapseTreeCategory } from "app/tree/thunks" import type { TreeSubscription } from "@/app/tree/slice"
import type { Category, Subscription } from "app/types" import { collapseTreeCategory } from "@/app/tree/thunks"
import { categoryUnreadCount, flattenCategoryTree } from "app/utils" import type { Category, Subscription } from "@/app/types"
import { Loader } from "components/Loader" import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "@/app/utils"
import { OnDesktop } from "components/responsive/OnDesktop" import { Loader } from "@/components/Loader"
import React from "react" import { OnDesktop } from "@/components/responsive/OnDesktop"
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
import { TreeNode } from "./TreeNode" import { TreeNode } from "./TreeNode"
import { TreeSearch } from "./TreeSearch" import { TreeSearch } from "./TreeSearch"
@@ -89,6 +90,7 @@ export function Tree() {
name={<Trans>All</Trans>} name={<Trans>All</Trans>}
icon={allIcon} icon={allIcon}
unread={categoryUnreadCount(root)} unread={categoryUnreadCount(root)}
hasNewEntries={categoryHasNewEntries(root)}
selected={source.type === "category" && source.id === Constants.categories.all.id} selected={source.type === "category" && source.id === Constants.categories.all.id}
expanded={false} expanded={false}
level={0} level={0}
@@ -103,6 +105,7 @@ export function Tree() {
name={<Trans>Starred</Trans>} name={<Trans>Starred</Trans>}
icon={starredIcon} icon={starredIcon}
unread={0} unread={0}
hasNewEntries={false}
selected={source.type === "category" && source.id === Constants.categories.starred.id} selected={source.type === "category" && source.id === Constants.categories.starred.id}
expanded={false} expanded={false}
level={0} level={0}
@@ -122,6 +125,7 @@ export function Tree() {
name={category.name} name={category.name}
icon={category.expanded ? expandedIcon : collapsedIcon} icon={category.expanded ? expandedIcon : collapsedIcon}
unread={categoryUnreadCount(category)} unread={categoryUnreadCount(category)}
hasNewEntries={categoryHasNewEntries(category)}
selected={source.type === "category" && source.id === category.id} selected={source.type === "category" && source.id === category.id}
expanded={category.expanded} expanded={category.expanded}
level={level} 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 if (!isFeedDisplayed(feed)) return null
return ( return (
@@ -143,6 +147,7 @@ export function Tree() {
name={feed.name} name={feed.name}
icon={feed.iconUrl} icon={feed.iconUrl}
unread={feed.unread} unread={feed.unread}
hasNewEntries={!!feed.hasNewEntries}
selected={source.type === "feed" && source.id === String(feed.id)} selected={source.type === "feed" && source.id === String(feed.id)}
level={level} level={level}
hasError={feed.errorCount > errorThreshold} hasError={feed.errorCount > errorThreshold}
@@ -159,6 +164,7 @@ export function Tree() {
name={tag} name={tag}
icon={tagIcon} icon={tagIcon}
unread={0} unread={0}
hasNewEntries={false}
selected={source.type === "tag" && source.id === tag} selected={source.type === "tag" && source.id === tag}
level={0} level={0}
hasError={false} hasError={false}
@@ -182,7 +188,7 @@ export function Tree() {
<OnDesktop> <OnDesktop>
<TreeSearch feeds={feeds} /> <TreeSearch feeds={feeds} />
</OnDesktop> </OnDesktop>
<Box> <Box className="cf-tree">
{allCategoryNode()} {allCategoryNode()}
{starredCategoryNode()} {starredCategoryNode()}
{root.children.map(c => recursiveCategoryNode(c))} {root.children.map(c => recursiveCategoryNode(c))}

View File

@@ -1,8 +1,8 @@
import { Box, Center } from "@mantine/core" import { Box, Center } from "@mantine/core"
import type { EntrySourceType } from "app/entries/slice"
import { FeedFavicon } from "components/content/FeedFavicon"
import type React from "react" 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" import { UnreadCount } from "./UnreadCount"
interface TreeNodeProps { interface TreeNodeProps {
@@ -15,6 +15,7 @@ interface TreeNodeProps {
expanded?: boolean expanded?: boolean
level: number level: number
hasError: boolean hasError: boolean
hasNewEntries: boolean
onClick: (e: React.MouseEvent, id: string) => void onClick: (e: React.MouseEvent, id: string) => void
onIconClick?: (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({ const { classes } = useStyles({
selected: props.selected, selected: props.selected,
hasError: props.hasError, hasError: props.hasError,
@@ -68,19 +69,19 @@ export function TreeNode(props: TreeNodeProps) {
<Box <Box
py={1} py={1}
pl={props.level * 20} pl={props.level * 20}
className={classes.node} className={`${classes.node} cf-treenode cf-treenode-${props.type}`}
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
data-id={props.id} data-id={props.id}
data-type={props.type} data-type={props.type}
data-unread-count={props.unread} data-unread-count={props.unread}
> >
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}> <Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)} className="cf-treenode-icon">
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center> <Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
</Box> </Box>
<Box className={classes.nodeText}>{props.name}</Box> <Box className={classes.nodeText}>{props.name}</Box>
{!props.expanded && ( {!props.expanded && (
<Box> <Box className="cf-treenode-unread-count">
<UnreadCount unreadCount={props.unread} /> <UnreadCount unreadCount={props.unread} showIndicator={props.hasNewEntries} />
</Box> </Box>
)} )}
</Box> </Box>

View File

@@ -1,20 +1,20 @@
import { msg } from "@lingui/core/macro" import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { TextInput } from "@mantine/core" import { Box, TextInput } from "@mantine/core"
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight" 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 { 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 { export interface TreeSearchProps {
feeds: Subscription[] feeds: Subscription[]
} }
export function TreeSearch(props: TreeSearchProps) { export function TreeSearch(props: Readonly<TreeSearchProps>) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { _ } = useLingui() const { _ } = useLingui()
@@ -33,7 +33,7 @@ export function TreeSearch(props: TreeSearchProps) {
useMousetrap("g u", () => spotlight.open()) useMousetrap("g u", () => spotlight.open())
return ( return (
<> <Box className="cf-treesearch">
<TextInput <TextInput
placeholder={_(msg`Search`)} placeholder={_(msg`Search`)}
leftSection={searchIcon} leftSection={searchIcon}
@@ -58,6 +58,6 @@ export function TreeSearch(props: TreeSearchProps) {
}} }}
nothingFound={<Trans>Nothing found</Trans>} nothingFound={<Trans>Nothing found</Trans>}
/> />
</> </Box>
) )
} }

View File

@@ -1,6 +1,6 @@
import { Badge, Tooltip } from "@mantine/core" import { Badge, Indicator, Tooltip } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "@/app/constants"
import { tss } from "tss" import { tss } from "@/tss"
const useStyles = tss.create(() => ({ const useStyles = tss.create(() => ({
badge: { 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() const { classes } = useStyles()
if (props.unreadCount <= 0) return null if (props.unreadCount <= 0) return null
@@ -18,9 +23,11 @@ export function UnreadCount(props: { unreadCount: number }) {
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
return ( return (
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}> <Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
<Badge className={classes.badge} variant="light" fullWidth> <Indicator disabled={!props.showIndicator} size={4} offset={10} position="middle-start">
{count} <Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
</Badge> {count}
</Badge>
</Indicator>
</Tooltip> </Tooltip>
) )
} }

View File

@@ -1,5 +1,5 @@
import { useMantineTheme } from "@mantine/core" import { useMantineTheme } from "@mantine/core"
import { useMobile } from "hooks/useMobile" import { useMobile } from "@/hooks/useMobile"
export const useActionButton = () => { export const useActionButton = () => {
const theme = useMantineTheme() const theme = useMantineTheme()

View File

@@ -1,6 +1,6 @@
import { msg } from "@lingui/core/macro" import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { useAppSelector } from "app/store" import { useAppSelector } from "@/app/store"
interface Step { interface Step {
label: string label: string
@@ -8,28 +8,28 @@ interface Step {
} }
export const useAppLoading = () => { export const useAppLoading = () => {
const profile = useAppSelector(state => state.user.profile) const profileLoaded = useAppSelector(state => !!state.user.profile)
const settings = useAppSelector(state => state.user.settings) const settingsLoaded = useAppSelector(state => !!state.user.settings)
const rootCategory = useAppSelector(state => state.tree.rootCategory) const rootCategoryLoaded = useAppSelector(state => !!state.tree.rootCategory)
const tags = useAppSelector(state => state.user.tags) const tagsLoaded = useAppSelector(state => !!state.user.tags)
const { _ } = useLingui() const { _ } = useLingui()
const steps: Step[] = [ const steps: Step[] = [
{ {
label: _(msg`Loading settings...`), label: _(msg`Loading settings...`),
done: !!settings, done: settingsLoaded,
}, },
{ {
label: _(msg`Loading profile...`), label: _(msg`Loading profile...`),
done: !!profile, done: profileLoaded,
}, },
{ {
label: _(msg`Loading subscriptions...`), label: _(msg`Loading subscriptions...`),
done: !!rootCategory, done: rootCategoryLoaded,
}, },
{ {
label: _(msg`Loading tags...`), label: _(msg`Loading tags...`),
done: !!tags, done: tagsLoaded,
}, },
] ]

View File

@@ -1,5 +1,5 @@
import { useMediaQuery } from "@mantine/hooks" import { useMediaQuery } from "@mantine/hooks"
import { Constants } from "app/constants" import { Constants } from "@/app/constants"
export const useMobile = (breakpoint: string | number = Constants.layout.mobileBreakpoint) => { export const useMobile = (breakpoint: string | number = Constants.layout.mobileBreakpoint) => {
const bp = typeof breakpoint === "number" ? `${breakpoint}px` : breakpoint const bp = typeof breakpoint === "number" ? `${breakpoint}px` : breakpoint

View File

@@ -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 { useEffect } from "react"
import WebsocketHeartbeatJs from "websocket-heartbeat-js" 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 handleMessage = (dispatch: AppDispatch, message: string) => {
const parts = message.split(":") const parts = message.split(":")

View File

@@ -1,7 +1,7 @@
import { type Messages, i18n } from "@lingui/core" import { i18n, type Messages } from "@lingui/core"
import { useAppSelector } from "app/store"
import dayjs from "dayjs" import dayjs from "dayjs"
import { useEffect } from "react" import { useEffect } from "react"
import { useAppSelector } from "@/app/store"
interface Locale { interface Locale {
key: string key: string
@@ -12,34 +12,146 @@ interface Locale {
// add an object to the array to add a new locale // add an object to the array to add a new locale
// don't forget to also add it to the 'locales' array in lingui.config.ts // don't forget to also add it to the 'locales' array in lingui.config.ts
export const locales: Locale[] = [ export const locales: Locale[] = [
{ key: "ar", label: "العربية", dayjsImportFn: async () => await import("dayjs/locale/ar") }, {
{ key: "ca", label: "Català", dayjsImportFn: async () => await import("dayjs/locale/ca") }, key: "ar",
{ key: "cs", label: "Čeština", dayjsImportFn: async () => await import("dayjs/locale/cs") }, label: "العربية",
{ key: "cy", label: "Cymraeg", dayjsImportFn: async () => await import("dayjs/locale/cy") }, dayjsImportFn: async () => await import("dayjs/locale/ar"),
{ 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: "ca",
{ key: "es", label: "Español", dayjsImportFn: async () => await import("dayjs/locale/es") }, label: "Català",
{ key: "fa", label: "فارسی", dayjsImportFn: async () => await import("dayjs/locale/fa") }, dayjsImportFn: async () => await import("dayjs/locale/ca"),
{ 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: "cs",
{ key: "hu", label: "Magyar", dayjsImportFn: async () => await import("dayjs/locale/hu") }, label: "Čeština",
{ key: "id", label: "Indonesian", dayjsImportFn: async () => await import("dayjs/locale/id") }, dayjsImportFn: async () => await import("dayjs/locale/cs"),
{ 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: "cy",
{ key: "ms", label: "Bahasa Malaysian", dayjsImportFn: async () => await import("dayjs/locale/ms") }, label: "Cymraeg",
{ key: "nb", label: "Norsk (bokmål)", dayjsImportFn: async () => await import("dayjs/locale/nb") }, dayjsImportFn: async () => await import("dayjs/locale/cy"),
{ 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: "da",
{ key: "pt", label: "Português", dayjsImportFn: async () => await import("dayjs/locale/pt") }, label: "Danish",
{ key: "ru", label: "Русский", dayjsImportFn: async () => await import("dayjs/locale/ru") }, dayjsImportFn: async () => await import("dayjs/locale/da"),
{ 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: "de",
{ key: "zh", label: "简体中文", dayjsImportFn: async () => await import("dayjs/locale/zh") }, 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) { function activateLocale(locale: string) {

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "رابط" msgstr "رابط"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "تحميل ملف التعريف ..." msgstr "تحميل ملف التعريف ..."
@@ -598,6 +602,10 @@ msgstr "الاسم"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "انتقل إلى اشتراك بإدخال اسمه" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -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>." msgstr "<0>CommaFeed és un projecte de codi obert. El codi font està allotjat a </0><1>GitHub</1>."
#: src/pages/app/FeedDetailsPage.tsx #: 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 "<0>La sintaxi completa està disponible </0><1>aquí</1>." msgstr "<0>La sintaxi completa està disponible </0><1>aquí</1><2>.</2>"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>" msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -135,7 +135,7 @@ msgstr "Tornar a iniciar sessió"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Blue" msgid "Blue"
msgstr "" msgstr "Blau"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome" msgid "Browser extension required for Chrome"
@@ -147,7 +147,7 @@ msgstr "Extensió del navegador"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Browser tab" msgid "Browser tab"
msgstr "" msgstr "Pestanya del navegador"
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
@@ -187,7 +187,7 @@ msgstr "Tanca el menu"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Cmd" msgid "Cmd"
msgstr "" msgstr "Cmd"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
@@ -247,7 +247,7 @@ msgstr "Codi JS personalitzat que s'executarà en carregar la pàgina"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Cyan" msgid "Cyan"
msgstr "" msgstr "Cian"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
@@ -334,11 +334,11 @@ msgstr "introduïu la vostra contrasenya actual per canviar la configuració del
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Entries to keep above the selected entry when scrolling" 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 #: src/components/settings/DisplaySettings.tsx
msgid "Entry headers" msgid "Entry headers"
msgstr "" msgstr "Encapçalaments d'entrada"
#: src/components/Alert.tsx #: src/components/Alert.tsx
msgid "Error" msgid "Error"
@@ -377,11 +377,11 @@ msgstr "Carrega tots els meus feeds ara"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Fever API" msgid "Fever API"
msgstr "" msgstr "Fever API"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL" msgid "Fever API URL"
msgstr "" msgstr "URL de Fever API"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression" msgid "Filtering expression"
@@ -389,11 +389,11 @@ msgstr "Expressió de filtratge"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Font size" msgid "Font size"
msgstr "" msgstr "Mida de la lletra"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available." 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 #: src/pages/auth/LoginPage.tsx
msgid "Forgot password?" msgid "Forgot password?"
@@ -434,15 +434,15 @@ msgstr "Bones"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "" msgstr "Raïm"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Gray" msgid "Gray"
msgstr "" msgstr "Gris"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Green" msgid "Green"
msgstr "" msgstr "Verd"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Id" msgid "Id"
@@ -466,11 +466,11 @@ msgstr "Importació"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "In expanded view, scrolling through entries mark them as read" 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 #: src/components/settings/DisplaySettings.tsx
msgid "Indigo" msgid "Indigo"
msgstr "" msgstr "Indi"
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
@@ -504,7 +504,7 @@ msgstr "Clar"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Lime" msgid "Lime"
msgstr "" msgstr "Llima"
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Enllaç" msgstr "Enllaç"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr "Enllaç a la documentació"
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Carregant el perfil..." msgstr "Carregant el perfil..."
@@ -540,7 +544,7 @@ msgstr "Tanca sessió"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press" msgid "Long press"
msgstr "" msgstr "Prem llargament la tecla"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
@@ -585,7 +589,7 @@ msgstr "Mou la pàgina cap amunt"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/components/RelativeDate.tsx #: src/components/RelativeDate.tsx
msgid "N/A" msgid "N/A"
msgstr "" msgstr "No es coneix"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -598,6 +602,10 @@ msgstr "Nom"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Navegueu a una subscripció introduint-ne el nom" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"
@@ -630,7 +638,7 @@ msgstr "No hi ha més entrades"
#: src/components/content/ShareButtons.tsx #: src/components/content/ShareButtons.tsx
msgid "No sharing options available." msgid "No sharing options available."
msgstr "" msgstr "No hi ha opcions de compartició disponibles."
#: src/components/sidebar/TreeSearch.tsx #: src/components/sidebar/TreeSearch.tsx
msgid "Nothing found" msgid "Nothing found"
@@ -642,11 +650,11 @@ msgstr "el més vell primer"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "On desktop" msgid "On desktop"
msgstr "" msgstr "A l'scriptori"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "On mobile" msgid "On mobile"
msgstr "" msgstr "Al mòbil"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen" msgid "On mobile, show action buttons at the bottom of the screen"
@@ -654,7 +662,7 @@ msgstr "Al mòbil, mostra els botons d'acció a la part inferior de la pantalla"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Only applies to compact, cozy and detailed modes" 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 #: src/pages/ErrorPage.tsx
msgid "Oops!" msgid "Oops!"
@@ -716,11 +724,11 @@ msgstr "Fitxer OPML"
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "OPML file is required" msgid "OPML file is required"
msgstr "" msgstr "Cal un fitxer OPML"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Orange" msgid "Orange"
msgstr "" msgstr "Taronja"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Order" msgid "Order"
@@ -752,7 +760,7 @@ msgstr "Les contrasenyes no coincideixen"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Pink" msgid "Pink"
msgstr "" msgstr "Rosa"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -765,7 +773,7 @@ msgstr "Anterior"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Primary color" msgid "Primary color"
msgstr "" msgstr "Color primari"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
@@ -777,7 +785,7 @@ msgstr "Recuperar la contrasenya"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Red" msgid "Red"
msgstr "" msgstr "Vermell"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
@@ -826,19 +834,19 @@ msgstr "Cerca"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Search requires at least 3 characters" 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 #: src/components/KeyboardShortcutsHelp.tsx
msgid "Select next unread feed/category" msgid "Select next unread feed/category"
msgstr "" msgstr "Selecciona el següent canal/categoria no llegit"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr "Selecciona el canal/categoria anterior sense llegir"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" 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 #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on previous entry without opening it" msgid "Set focus on previous entry without opening it"
@@ -858,7 +866,7 @@ msgstr "Comparteix"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartir llocs" msgstr "Compartir a altres llocs web"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
@@ -884,7 +892,7 @@ msgstr "Mostra el menú d'entrada (mòbil)"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show external link icon" msgid "Show external link icon"
msgstr "" msgstr "Mostra la icona d'enllaç extern"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries" msgid "Show feeds and categories with no unread entries"
@@ -900,15 +908,15 @@ msgstr "Mostra el menú natiu (escriptori)"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show star icon" msgid "Show star icon"
msgstr "" msgstr "Mostra la icona d'estrella"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab favicon" 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 #: src/components/settings/DisplaySettings.tsx
msgid "Show unread count in tab title" 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/WelcomePage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -978,7 +986,7 @@ msgstr "Etiquetes"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Teal" msgid "Teal"
msgstr "" msgstr "Blau verdós"
#: src/components/content/add/Subscribe.tsx #: 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." 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."
@@ -1038,7 +1046,7 @@ msgstr "Nom d'usuari o correu electrònic"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Violet" msgid "Violet"
msgstr "" msgstr "Violeta"
#: src/components/Alert.tsx #: src/components/Alert.tsx
msgid "Warning" msgid "Warning"
@@ -1050,7 +1058,7 @@ msgstr "Lloc web"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "" msgstr "Groc"
#: src/pages/app/FeedEntriesPage.tsx #: 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?" msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Odkaz" msgstr "Odkaz"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Načítání profilu..." msgstr "Načítání profilu..."
@@ -598,6 +602,10 @@ msgstr "Jméno"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Přejděte na předplatné zadáním jeho názvu" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Cyswllt" msgstr "Cyswllt"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Wrthi'n llwytho proffil..." msgstr "Wrthi'n llwytho proffil..."
@@ -598,6 +602,10 @@ msgstr "Enw"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Llywiwch i danysgrifiad trwy nodi ei enw" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "" msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Indlæser profil..." msgstr "Indlæser profil..."
@@ -598,6 +602,10 @@ msgstr "Navn"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Naviger til et abonnement ved at indtaste dets navn" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -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." msgstr "<0>CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
#: src/pages/app/FeedDetailsPage.tsx #: 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 "<0>Die vollständige Syntax ist </0><1>hier</1> verfügbar." msgstr "<0>Die vollständige Syntax ist </0><1>hier</1> verfügbar<2>.</2>"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>" msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Verbindung" msgstr "Verbindung"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Lade Profil..." msgstr "Lade Profil..."
@@ -598,6 +602,10 @@ msgstr ""
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Navigieren Sie zu einem Abonnement, indem Sie seinen Namen eingeben" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -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>." msgstr "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
#: src/pages/app/FeedDetailsPage.tsx #: 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 "<0>Complete syntax is available </0><1>here</1>." msgstr "<0>Complete syntax is available </0><1>here</1><2>.</2>"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>" msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -512,6 +512,10 @@ msgstr "Lime"
msgid "Link" msgid "Link"
msgstr "Link" msgstr "Link"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr "Link to the documentation"
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Loading profile..." msgstr "Loading profile..."
@@ -598,6 +602,10 @@ msgstr "Name"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -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>." 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 #: 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 "<0>La sintaxis completa está disponible </0><1>aquí</1>." msgstr "<0>La sintaxis completa está disponible </0><1>aquí</1><2>.</2>"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>" msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -513,6 +513,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Enlace" msgstr "Enlace"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Cargando perfil..." msgstr "Cargando perfil..."
@@ -599,6 +603,10 @@ msgstr "Nombre"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Navegar a una suscripción introduciendo su nombre" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "پیوند" msgstr "پیوند"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "بارگیری نمایه..." msgstr "بارگیری نمایه..."
@@ -598,6 +602,10 @@ msgstr "نام"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "با وارد کردن نام اشتراک، به آن بروید" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Linkki" msgstr "Linkki"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Ladataan profiilia..." msgstr "Ladataan profiilia..."
@@ -598,6 +602,10 @@ msgstr "Nimi"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Siirry tilaukseen kirjoittamalla sen nimi" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -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>." msgstr "<0>CommaFeed est un projet open-source. Les sources sont hébergées sur </0><1>GitHub</1>."
#: src/pages/app/FeedDetailsPage.tsx #: 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 "<0>La syntaxe complète est disponible </0><1>ici</1>." msgstr "<0>La syntaxe complète est disponible </0><1>ici</1><2>.</2>"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>" msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -512,6 +512,10 @@ msgstr "Jaune-vert"
msgid "Link" msgid "Link"
msgstr "Lien" msgstr "Lien"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr "Lien vers la documentation"
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Chargement du profil..." msgstr "Chargement du profil..."
@@ -598,6 +602,10 @@ msgstr "Nom"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Naviguer vers un abonnement en entrant son nom" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Ligazón" msgstr "Ligazón"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Cargando perfil..." msgstr "Cargando perfil..."
@@ -598,6 +602,10 @@ msgstr "Nome"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Navega a unha subscrición introducindo o seu nome" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "" msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Profil betöltése..." msgstr "Profil betöltése..."
@@ -598,6 +602,10 @@ msgstr "Név"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Navigáljon egy előfizetéshez a nevének megadásával" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

View File

@@ -18,7 +18,7 @@ msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitH
msgstr "" msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: 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 "" msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
@@ -512,6 +512,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "Tautan" msgstr "Tautan"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Link to the documentation"
msgstr ""
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Memuat profil..." msgstr "Memuat profil..."
@@ -598,6 +602,10 @@ msgstr "Nama"
msgid "Navigate to a subscription by entering its name" msgid "Navigate to a subscription by entering its name"
msgstr "Navigasikan ke langganan dengan memasukkan namanya" 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
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Never" msgid "Never"

Some files were not shown because too many files have changed in this diff Show More