Compare commits

...

1248 Commits

Author SHA1 Message Date
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
850921bca9 release 5.8.0 2025-05-06 20:07:54 +02:00
Athou
1dc6470419 move github sponsors up to match the order on the github page 2025-05-06 20:05:33 +02: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
renovate[bot]
ebca9f8290 fix(deps): update dependency tss-react to ^4.9.17 2025-05-06 07:51:25 +00:00
renovate[bot]
53b1f89b30 chore(deps): update dependency @types/react to ^19.1.3 2025-05-06 03:41:03 +00:00
renovate[bot]
6885191877 chore(deps): update ibm-semeru-runtimes:open-21.0.6_7-jre docker digest to bb59a7e 2025-05-05 22:59:50 +00:00
renovate[bot]
e69d9fe8b8 chore(deps): update dependency vitest to ^3.1.3 2025-05-05 17:13:14 +00:00
Jérémie Panzer
d6a1f1ae15 Merge pull request #1773 from canoine/patch-4
Update fr/messages.po
2025-05-05 19:12:16 +02:00
canoine
a7813f4442 Update fr/messages.po
Translation of the new message
2025-05-05 19:03:00 +02:00
Athou
1e4664987a add missing i18n label 2025-05-05 17:28:52 +02:00
Jérémie Panzer
7a819f5d58 Merge pull request #1772 from Athou/renovate/major-mantine-monorepo
fix(deps): update mantine monorepo to v8 (major)
2025-05-05 14:00:14 +02:00
Athou
45ef56e9da reduce vertical room occupied by slider 2025-05-05 13:59:23 +02:00
Athou
f43c7aa5d0 change default value of entriesToKeepOnTopWhenScrolling to match what other feed readers are doing 2025-05-05 13:51:32 +02:00
renovate[bot]
8d88711e59 fix(deps): update mantine monorepo to v8 2025-05-05 11:49:11 +00:00
renovate[bot]
280c0b60e9 chore(deps): update dependency vite to ^6.3.5 2025-05-05 10:01:35 +00:00
renovate[bot]
605f8f6615 chore(deps): lock file maintenance 2025-05-05 01:29:24 +00:00
renovate[bot]
0e88b4de1b chore(deps): update dependency vite-plugin-checker to ^0.9.2 2025-05-04 13:26:48 +00:00
Athou
b02aa923d7 customizable font size (#1462) 2025-05-04 13:26:33 +02:00
Athou
680c927e1d font size reduction was not actually working, and looks bad now that it does 2025-05-04 01:21:07 +02:00
Athou
4b2e65abdc use Box instead of Text because Text resets font size 2025-05-04 01:16:11 +02:00
Athou
e501bf6b05 fix initial value not loaded correctly for newly added settings 2025-05-03 21:50:53 +02:00
Jérémie Panzer
80bf2582bd Merge pull request #1771 from Athou/renovate/com.microsoft.playwright-playwright-1.x
chore(deps): update dependency com.microsoft.playwright:playwright to v1.52.0
2025-05-02 22:14:41 +02:00
renovate[bot]
586da4424d chore(deps): update dependency com.microsoft.playwright:playwright to v1.52.0 2025-05-02 18:59:58 +00:00
renovate[bot]
8e0e8c2407 fix(deps): update mantine monorepo to ^7.17.7 2025-05-02 10:10:57 +00:00
Athou
40461ac883 fix wrong category mapping 2025-05-01 13:01:28 +02:00
renovate[bot]
9288a7e66e chore(deps): update dependency vite to ^6.3.4 2025-04-30 19:48:04 +00:00
Jérémie Panzer
275db4ec72 Merge pull request #1770 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.22.1 (minor)
2025-04-30 21:47:10 +02:00
renovate[bot]
67b2f8968d fix(deps): update quarkus.version to v3.22.1 2025-04-30 19:04:04 +00:00
renovate[bot]
45ce35dfdb chore(deps): update dependency @types/react-dom to ^19.1.3 2025-04-30 13:32:29 +00:00
renovate[bot]
52aa9ab2fe chore(deps): update debian:12.10 docker digest to 264982f 2025-04-29 11:18:53 +00:00
Jérémie Panzer
d24725bd55 Merge pull request #1769 from Athou/renovate/org.jsoup-jsoup-1.x
fix(deps): update dependency org.jsoup:jsoup to v1.20.1
2025-04-29 13:18:04 +02:00
renovate[bot]
f368a67dec fix(deps): update dependency org.jsoup:jsoup to v1.20.1 2025-04-29 08:40:01 +00:00
Jérémie Panzer
564d1744e1 Merge pull request #1768 from canoine/patch-2
Update fr/messages.po
2025-04-29 10:39:05 +02:00
canoine
d091ecfa5f Update fr/messages.po
Translation of new messages
2025-04-29 09:22:39 +02:00
Jérémie Panzer
7d23165e14 Merge pull request #1764 from WangLei1993/master
add Chinese translation for new entry
2025-04-29 06:33:39 +02:00
WangLei1993
a9ca3278c6 add Chinese translation for color 2025-04-29 11:55:32 +08:00
WangLei1993
e8734710ca Merge branch 'Athou:master' into master 2025-04-29 10:42:39 +08:00
renovate[bot]
f8a0e20df9 fix(deps): update dependency react-router-dom to ^7.5.3 2025-04-28 22:47:15 +00:00
Athou
de90e4de54 try to find a suitable language for the user before defaulting to english (#1767) 2025-04-28 22:59:41 +02:00
Athou
03cb27f69a add new translation placeholders 2025-04-28 22:49:57 +02:00
Athou
f86f1dd770 'All' and 'Starred' labels are now translatable (#1765) 2025-04-28 22:48:02 +02:00
Athou
ec21ffc571 color names are now translatable (#1766) 2025-04-28 22:39:15 +02:00
WangLei1993
c73c9c74ba Merge branch 'Athou:master' into master 2025-04-29 03:25:58 +08:00
WangLei1993
fd3c264d0c add Chinese translation for new entry 2025-04-29 03:25:16 +08:00
renovate[bot]
3983ba6cd0 chore(deps): lock file maintenance 2025-04-28 08:49:58 +00:00
renovate[bot]
dc0b5bdd11 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.23.1 2025-04-28 03:08:48 +00:00
renovate[bot]
5b3728186e fix(deps): update dependency react-router-dom to ^7.5.2 2025-04-25 06:22:05 +00:00
renovate[bot]
8b0936b678 fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.4.4 2025-04-25 05:11:21 +00:00
Jérémie Panzer
b99e81a389 Merge pull request #1763 from Athou/renovate/axios-1.x
fix(deps): update dependency axios to ^1.9.0
2025-04-25 07:10:40 +02:00
renovate[bot]
0b8fb0f9a7 fix(deps): update dependency axios to ^1.9.0 2025-04-24 23:53:43 +00:00
renovate[bot]
7d5bbe0130 chore(deps): update docker/build-push-action digest to 14487ce 2025-04-24 23:53:15 +00:00
renovate[bot]
c6637c6814 chore(deps): update actions/download-artifact digest to d3f86a1 2025-04-24 20:09:51 +00:00
renovate[bot]
e302a011bb fix(deps): update mantine monorepo to ^7.17.5 2025-04-24 12:41:34 +00:00
renovate[bot]
25f8bdaa28 chore(deps): update dependency vite to ^6.3.3 2025-04-24 09:42:45 +00:00
renovate[bot]
e7d1018cbc chore(deps): update dependency io.quarkus.platform:quarkus-maven-plugin to v3.21.4 2025-04-23 22:14:40 +00:00
Jérémie Panzer
e81fa69a03 Merge pull request #1761 from Athou/renovate/node-22.x
chore(deps): update node.js to v22.15.0
2025-04-23 15:22:37 +02:00
renovate[bot]
176e76ad2d chore(deps): update node.js to v22.15.0 2025-04-23 12:58:46 +00:00
Jérémie Panzer
0925a91089 Merge pull request #1759 from Athou/renovate/org.apache.commons-commons-collections4-4.x
fix(deps): update dependency org.apache.commons:commons-collections4 to v4.5.0
2025-04-23 07:24:00 +02:00
renovate[bot]
c04d0b147c fix(deps): update dependency org.apache.commons:commons-collections4 to v4.5.0 2025-04-22 20:08:59 +00:00
renovate[bot]
a349eff1a3 chore(deps): update dependency vitest to ^3.1.2 2025-04-21 10:23:42 +00:00
renovate[bot]
2ddeda9a27 chore(deps): lock file maintenance 2025-04-21 02:01:50 +00:00
renovate[bot]
00b0fe921f chore(deps): update dependency @vitejs/plugin-react to ^4.4.1 2025-04-19 15:03:29 +00:00
renovate[bot]
bcc6cdf4b1 chore(deps): update dependency vite to ^6.3.2 2025-04-18 07:33:49 +00:00
renovate[bot]
ba46cc3cd6 fix(deps): update dependency react-router-dom to ^7.5.1 2025-04-17 15:41:39 +00:00
renovate[bot]
ed1bf609b8 chore(deps): update dependency vite to ^6.3.1 2025-04-17 02:11:49 +00:00
Jérémie Panzer
13262b678d Merge pull request #1756 from Athou/renovate/reduxjs-toolkit-2.x
fix(deps): update dependency @reduxjs/toolkit to ^2.7.0
2025-04-16 21:53:25 +02:00
renovate[bot]
cf9321de23 fix(deps): update dependency @reduxjs/toolkit to ^2.7.0 2025-04-16 18:48:12 +00:00
renovate[bot]
ac7a78bdc0 fix(deps): update quarkus.version to v3.21.3 2025-04-16 15:27:22 +00:00
Jérémie Panzer
640d3f1f6e Merge pull request #1754 from Athou/renovate/vite-6.x
chore(deps): update dependency vite to ^6.3.0
2025-04-16 11:45:25 +02:00
renovate[bot]
d716a8081c chore(deps): update dependency vite to ^6.3.0 2025-04-16 09:03:14 +00:00
Athou
1cec4e68b1 remove unused variables 2025-04-16 01:16:34 +02:00
Jérémie Panzer
9d16299c5b Merge pull request #1753 from Athou/renovate/vitejs-plugin-react-4.x
chore(deps): update dependency @vitejs/plugin-react to ^4.4.0
2025-04-15 14:22:10 +02:00
renovate[bot]
cb92b1969c chore(deps): update dependency @vitejs/plugin-react to ^4.4.0 2025-04-15 10:50:23 +00:00
renovate[bot]
55a62d393d chore(deps): update dependency @types/react to ^19.1.2 2025-04-14 23:14:48 +00:00
renovate[bot]
027b2252db fix(deps): update linguijs monorepo to ^5.3.1 2025-04-14 17:27:06 +00:00
Jérémie Panzer
ccfb88ddcc Merge pull request #1752 from Athou/renovate/redoc-2.x
fix(deps): update dependency redoc to ^2.5.0
2025-04-14 19:26:10 +02:00
renovate[bot]
dde0736fcf fix(deps): update dependency redoc to ^2.5.0 2025-04-14 16:24:14 +00:00
renovate[bot]
6498bb5ee6 chore(deps): lock file maintenance 2025-04-14 01:54:18 +00:00
Jérémie Panzer
d7ca2db330 Merge pull request #1751 from Athou/renovate/jsdom-26.x
chore(deps): update dependency jsdom to ^26.1.0
2025-04-13 18:11:54 +02:00
renovate[bot]
99b32795a5 chore(deps): update dependency jsdom to ^26.1.0 2025-04-13 14:35:09 +00:00
renovate[bot]
840f670d5d chore(deps): update dependency @types/react to ^19.1.1 2025-04-11 15:51:04 +00:00
renovate[bot]
e3c0a4c665 chore(deps): update dependency vite to ^6.2.6 2025-04-10 13:00:03 +00:00
Athou
915506527a add theme color picker (#1598) 2025-04-10 14:17:44 +02:00
renovate[bot]
7541251344 fix(deps): update quarkus.version to v3.21.2 2025-04-09 22:01:45 +00:00
Athou
40c9b42b24 mark all as read confirmation now also applies to the "shift+a" keyboard shortcut (#1744) 2025-04-09 20:28:40 +02:00
Athou
dfab678070 remove unused imports 2025-04-09 20:28:40 +02:00
renovate[bot]
3d371d5942 chore(deps): update dependency @types/react-dom to ^19.1.2 2025-04-09 17:25:25 +00:00
renovate[bot]
9bfbaa8ded chore(deps): update ibm-semeru-runtimes:open-21.0.6_7-jre docker digest to 475c325 2025-04-09 16:47:23 +00:00
Athou
be0fc95c45 dialog can now by confirmed with the enter key 2025-04-09 14:40:53 +02:00
Athou
dcb6113eb7 set focus to slider when the dialog opens to ease keyboard navigation 2025-04-09 14:40:53 +02:00
renovate[bot]
55491651f6 chore(deps): update ibm-semeru-runtimes:open-21.0.6_7-jre docker digest to 1515f0d 2025-04-09 07:58:05 +00:00
Jérémie Panzer
c1d471ebdc Merge pull request #1750 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11.3.0
2025-04-09 07:19:10 +02:00
renovate[bot]
069e675f19 chore(deps): update dependency npm to v11.3.0 2025-04-08 22:06:01 +00:00
renovate[bot]
d34f719a4f fix(deps): update mantine monorepo to ^7.17.4 2025-04-08 16:57:23 +00:00
renovate[bot]
4eb932a3f0 chore(deps): update debian:12.10 docker digest to 00cd074 2025-04-08 06:24:19 +00:00
renovate[bot]
1ab27f2626 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.4 2025-04-08 03:28:41 +00:00
renovate[bot]
7d3ce7e602 fix(deps): update swagger.version to v2.2.30 2025-04-07 16:46:41 +00:00
renovate[bot]
cf530b2c60 chore(deps): lock file maintenance 2025-04-07 01:34:57 +00:00
Athou
d9bcd7f592 remove warning about Non-Serializable Data passed to redux action 2025-04-05 17:19:04 +02:00
Athou
8b854b5cda remove intermediate element by rendering the mantine Paper as <article> 2025-04-05 17:19:04 +02:00
renovate[bot]
f7b6677bb1 chore(deps): update dependency typescript to ^5.8.3 2025-04-05 04:17:26 +00:00
Jérémie Panzer
0cc5e8f5b8 Merge pull request #1748 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.5.0
2025-04-05 06:16:57 +02:00
Jérémie Panzer
8d153e3b2b Merge pull request #1749 from Athou/renovate/querydsl.version
fix(deps): update querydsl.version to v6.11 (minor)
2025-04-05 06:16:45 +02:00
renovate[bot]
d15428971c fix(deps): update querydsl.version to v6.11 2025-04-05 02:02:22 +00:00
renovate[bot]
f34c2aa437 fix(deps): update dependency react-router-dom to ^7.5.0 2025-04-04 23:12:23 +00:00
Athou
9b3ff5f81f fix wrong issue numbers in changelog 2025-04-04 22:15:37 +02:00
Athou
e1f6937802 release 5.7.0 2025-04-04 19:39:29 +02:00
Athou
0c0834b30f fix bookmarklet, React 19 no longer allows 'javascript:' urls 2025-04-04 14:25:21 +02:00
Jérémie Panzer
5ad4b97205 Merge pull request #1747 from flisk/patch-1
don't throw NotModifiedException on etag/lm changes
2025-04-04 11:18:57 +02:00
flisk
c4ec249bc4 don't throw NotModifiedException on etag/lm changes
A well-behaved server should return 304 if our If-Modified-Since and If-None-Match indicate that we don't have the latest version of a resource cached. Having these extra conditions where we consider our local version fresh is not necessary, and may in fact lead to resource updates being missed when only one header changes.

We should instead trust the server to know whether it needs to send us a new resource or not based on the cache headers we provide.
2025-04-04 11:00:47 +02:00
renovate[bot]
cf8d3965d5 chore(deps): update dependency vite to ^6.2.5 2025-04-03 14:51:19 +00:00
renovate[bot]
3903fd9374 chore(deps): update peter-evans/dockerhub-description digest to 432a30c 2025-04-03 09:42:24 +00:00
Jérémie Panzer
77d59dabe8 Merge pull request #1742 from Athou/renovate/testing-library-monorepo
chore(deps): update dependency @testing-library/react to ^16.3.0
2025-04-03 00:37:51 +02:00
Jérémie Panzer
56ca737297 Merge pull request #1743 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.23.0
2025-04-03 00:37:34 +02:00
renovate[bot]
9edb539be3 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.23.0 2025-04-02 19:13:17 +00:00
renovate[bot]
31a773d200 chore(deps): update dependency @testing-library/react to ^16.3.0 2025-04-02 19:13:13 +00:00
renovate[bot]
61355eabf7 chore(deps): update dependency io.quarkus.platform:quarkus-maven-plugin to v3.21.1 2025-04-02 19:12:53 +00:00
Jérémie Panzer
569874e51f Merge pull request #1741 from Athou/renovate/react-monorepo
chore(deps): update react monorepo (minor)
2025-04-02 21:53:31 +09:00
renovate[bot]
00d47901fc chore(deps): update react monorepo 2025-04-02 10:49:12 +00:00
renovate[bot]
d8b4ef55ce chore(deps): lock file maintenance 2025-03-31 22:26:46 +00:00
renovate[bot]
da41a4cab9 fix(deps): update dependency org.projectlombok:lombok to v1.18.38 2025-03-31 19:13:21 +00:00
Jérémie Panzer
8a90ef0471 Merge pull request #1740 from Athou/renovate/vitest-monorepo
chore(deps): update dependency vitest to ^3.1.1
2025-04-01 04:12:48 +09:00
renovate[bot]
b4ab32a578 chore(deps): update dependency vite to ^6.2.4 2025-03-31 16:02:10 +00:00
renovate[bot]
03aa53abc8 chore(deps): update dependency vitest to ^3.1.1 2025-03-31 11:06:04 +00:00
renovate[bot]
2ae5c0cd8e chore(deps): update peter-evans/dockerhub-description digest to 0505d8b 2025-03-31 11:05:32 +00:00
renovate[bot]
cacc632443 chore(deps): update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.3 2025-03-31 07:27:09 +00:00
renovate[bot]
28f865ccfa chore(deps): update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.3 2025-03-31 03:07:34 +00:00
Jérémie Panzer
a4c949e8b3 Merge pull request #1739 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.22.0
2025-03-31 04:49:23 +09:00
renovate[bot]
6098994397 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.22.0 2025-03-30 17:50:39 +00:00
renovate[bot]
5763ca30d6 fix(deps): update dependency react-router-dom to ^7.4.1 2025-03-29 00:17:28 +00:00
Jérémie Panzer
7d039d1001 Merge pull request #1738 from Athou/renovate/react-monorepo
fix(deps): update react monorepo to ^19.1.0 (minor)
2025-03-29 09:16:31 +09:00
renovate[bot]
7fe74af906 fix(deps): update react monorepo to ^19.1.0 2025-03-28 23:25:45 +00:00
renovate[bot]
80b72aa30b fix(deps): update dependency tss-react to ^4.9.16 2025-03-28 10:05:38 +00:00
renovate[bot]
3ba0d241f9 fix(deps): update mantine monorepo to ^7.17.3 2025-03-27 12:04:33 +00:00
renovate[bot]
67428aa0c7 fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.4.3 2025-03-26 23:20:17 +00:00
Jérémie Panzer
b9a0256031 Merge pull request #1737 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.21.0 (minor)
2025-03-27 08:19:41 +09:00
renovate[bot]
f3c2296636 fix(deps): update quarkus.version to v3.21.0 2025-03-26 12:33:20 +00:00
renovate[bot]
b6e8f21975 chore(deps): update ibm-semeru-runtimes:open-21.0.6_7-jre docker digest to fc0d0c8 2025-03-25 01:44:57 +00:00
renovate[bot]
284f80045f chore(deps): update dependency vite to ^6.2.3 2025-03-24 12:07:42 +00:00
renovate[bot]
f589477aa8 chore(deps): lock file maintenance 2025-03-24 02:45:32 +00:00
Jérémie Panzer
29cb296d09 Merge pull request #1733 from Athou/renovate/linguijs-monorepo
fix(deps): update linguijs monorepo to ^5.3.0 (minor)
2025-03-21 21:07:34 +09:00
renovate[bot]
86caa1450a fix(deps): update linguijs monorepo to ^5.3.0 2025-03-21 11:26:04 +00:00
Jérémie Panzer
9dd4b9e67f Merge pull request #1732 from Athou/renovate/patch-react-monorepo
chore(deps): update dependency @types/react to ^19.0.12
2025-03-21 07:09:17 +09:00
renovate[bot]
e2e654f05b chore(deps): update dependency @types/react to ^19.0.12 2025-03-20 16:27:41 +00:00
renovate[bot]
72dbc62b41 fix(deps): update quarkus.version to v3.19.4 2025-03-20 10:58:22 +00:00
renovate[bot]
0a21014668 fix(deps): update dependency axios to ^1.8.4 2025-03-20 06:35:53 +00:00
renovate[bot]
b6d9d2a26c chore(deps): update actions/upload-artifact digest to ea165f8 2025-03-20 01:47:55 +00:00
renovate[bot]
25c3a7748c chore(deps): update actions/download-artifact digest to 95815c3 2025-03-19 20:49:20 +00:00
Jérémie Panzer
b2bcfdd6eb Merge pull request #1731 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.4.0
2025-03-19 21:48:46 +01:00
renovate[bot]
2a978db406 fix(deps): update dependency react-router-dom to ^7.4.0 2025-03-19 17:04:09 +00:00
renovate[bot]
9e40d0d066 chore(deps): update actions/download-artifact digest to cc20338 2025-03-19 02:40:14 +00:00
renovate[bot]
c912650d59 chore(deps): update actions/download-artifact digest to b14cf4c 2025-03-18 19:01:24 +00:00
renovate[bot]
464ebcb471 chore(deps): lock file maintenance 2025-03-18 12:30:06 +00:00
renovate[bot]
463e0e59d7 chore(deps): update debian:12.10 docker digest to 18023f1 2025-03-18 07:34:16 +00:00
Jérémie Panzer
b4e5d8ef20 Merge pull request #1729 from Athou/renovate/patch-react-monorepo
chore(deps): update dependency @types/react to ^19.0.11
2025-03-18 08:33:48 +01:00
renovate[bot]
126905aeb3 chore(deps): update dependency @types/react to ^19.0.11 2025-03-18 04:22:16 +00:00
Jérémie Panzer
1af10d3364 Merge pull request #1727 from Athou/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2025-03-18 05:20:59 +01:00
Jérémie Panzer
6ad854c019 Merge pull request #1728 from Athou/renovate/com.microsoft.playwright-playwright-1.x
chore(deps): update dependency com.microsoft.playwright:playwright to v1.51.0
2025-03-18 05:20:37 +01:00
Jérémie Panzer
b30117aa4d Merge pull request #1730 from Athou/renovate/debian-12.x
chore(deps): update debian docker tag to v12.10
2025-03-18 05:19:56 +01:00
renovate[bot]
5a66482d1e chore(deps): lock file maintenance 2025-03-18 02:30:37 +00:00
renovate[bot]
2628ec49bb chore(deps): update debian docker tag to v12.10 2025-03-18 02:29:44 +00:00
renovate[bot]
f3d15cf173 chore(deps): update dependency com.microsoft.playwright:playwright to v1.51.0 2025-03-17 23:39:46 +00:00
renovate[bot]
bbcf55ce57 chore(deps): update dependency vitest to ^3.0.9 2025-03-17 23:39:43 +00:00
renovate[bot]
72fc3716e7 chore(deps): update dependency vite-plugin-checker to ^0.9.1 2025-03-17 17:30:58 +00:00
Jérémie Panzer
81a6cfaa88 Merge pull request #1725 from Athou/renovate/com.ibm.icu-icu4j-77.x
fix(deps): update dependency com.ibm.icu:icu4j to v77
2025-03-15 01:14:58 +01:00
renovate[bot]
aed5165ef3 chore(deps): update dependency vite to ^6.2.2 (#1726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 23:14:20 +00:00
renovate[bot]
eaf2933726 fix(deps): update mantine monorepo to ^7.17.2 2025-03-14 15:16:11 +00:00
renovate[bot]
39da4d9d36 chore(deps): update docker/login-action digest to 74a5d14 2025-03-14 11:05:08 +00:00
renovate[bot]
e5ebd7ff39 fix(deps): update dependency com.ibm.icu:icu4j to v77 2025-03-14 04:29:10 +00:00
renovate[bot]
b6ae3e4e1e fix(deps): update dependency io.quarkus.platform:quarkus-bom to v3.19.3 2025-03-12 16:05:37 +00:00
renovate[bot]
32d1488352 fix(deps): update dependency axios to ^1.8.3 2025-03-12 10:40:24 +00:00
Athou
b08d0a388f no need to re-render the custom js handler on route change 2025-03-12 10:36:17 +01:00
Athou
7fe004a696 load js when the app is done loading (#1724) 2025-03-12 07:33:44 +01:00
Athou
f620d033b0 DisablePullToRefresh doesn't need to be in HashRouter 2025-03-12 07:21:31 +01:00
Athou
ba071ba71f add the referrer meta (#1724) 2025-03-12 06:37:12 +01:00
Athou
6f3197302d make biome format root files too (package.json, vite config, ...) 2025-03-11 15:50:04 +01:00
Athou
131a8ebf68 remove warning about vite not finding custom code at build time 2025-03-11 15:48:52 +01:00
Athou
8b24c125c2 also skip js tests when skipping tests 2025-03-11 15:36:32 +01:00
Jérémie Panzer
52293376ec Merge pull request #1723 from Athou/renovate/patch-swagger.version
fix(deps): update swagger.version to v2.2.29 (patch)
2025-03-11 07:23:52 +01:00
renovate[bot]
f8ac59af6a fix(deps): update swagger.version to v2.2.29 2025-03-10 23:04:46 +00:00
Athou
5c791e2305 revert corrupted png files 2025-03-10 11:03:43 +01:00
Athou
6641bc0631 don't expose exception message 2025-03-10 10:54:40 +01:00
Jérémie Panzer
da690aa750 Merge pull request #1722 from Athou/line-endings
normalize line endings
2025-03-10 10:50:11 +01:00
Athou
fb7f041454 normalize line endings 2025-03-10 08:48:26 +01:00
renovate[bot]
ec4554c76e chore(deps): lock file maintenance 2025-03-10 03:01:10 +00:00
Athou
068e85fe6e add tests for selectNextUnreadTreeItem 2025-03-08 08:32:46 +01:00
renovate[bot]
ba926c674e fix(deps): update dependency @reduxjs/toolkit to ^2.6.1 2025-03-07 22:23:31 +00:00
renovate[bot]
836f8f14c0 fix(deps): update dependency axios to ^1.8.2 2025-03-07 10:56:42 +00:00
renovate[bot]
eeecac96e1 chore(deps): update dependency vite to ^6.2.1 2025-03-07 05:02:32 +00:00
Jérémie Panzer
ecc62f222a Merge pull request #1721 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.3.0
2025-03-07 06:01:39 +01:00
renovate[bot]
9022f93811 fix(deps): update dependency react-router-dom to ^7.3.0 2025-03-06 22:53:13 +00:00
renovate[bot]
e7225d35b2 chore(deps): update dependency vitest to ^3.0.8 2025-03-06 20:01:28 +00:00
Athou
454fc03038 automerge digest changes 2025-03-06 21:00:28 +01:00
Jérémie Panzer
9c0674fd83 Merge pull request #1720 from Athou/renovate/migrate-config
chore(config): migrate renovate config
2025-03-06 20:57:05 +01:00
Jérémie Panzer
7a20482ddf Merge pull request #1719 from Athou/renovate/pin-dependencies
chore(deps): pin dependencies
2025-03-06 20:55:28 +01:00
renovate[bot]
32ad47ba16 chore(config): migrate config renovate.json 2025-03-06 16:52:01 +00:00
renovate[bot]
fc562cce0f chore(deps): pin dependencies 2025-03-06 16:51:34 +00:00
Athou
b029b251db use renovate best practices 2025-03-06 17:50:54 +01:00
Jérémie Panzer
e3e28e727f Merge pull request #1718 from Athou/renovate/io.github.hakky54-sslcontext-kickstart-for-apache5-9.x
fix(deps): update dependency io.github.hakky54:sslcontext-kickstart-for-apache5 to v9.1.0
2025-03-06 07:08:24 +01:00
Jérémie Panzer
50cb728db7 Merge pull request #1717 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11.2.0
2025-03-06 07:08:12 +01:00
renovate[bot]
c654ba4d1b fix(deps): update dependency io.github.hakky54:sslcontext-kickstart-for-apache5 to v9.1.0 2025-03-05 21:32:08 +00:00
renovate[bot]
846e29b15e chore(deps): update dependency npm to v11.2.0 2025-03-05 21:32:05 +00:00
renovate[bot]
f2b4062d73 fix(deps): update quarkus.version to v3.19.2 2025-03-05 19:37:47 +00:00
Athou
9051e6a6db add test to make sure documentation is available 2025-03-05 17:22:53 +01:00
Athou
b733129043 remove the need for selectPreviousUnreadTreeItem by reversing the array if we're going backwards 2025-03-05 07:33:36 +01:00
Athou
d46b571444 select next/previous unread feed/category when marking all as read (#1558) 2025-03-04 21:18:45 +01:00
renovate[bot]
7d744b4ce0 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.4 2025-03-04 13:07:31 +00:00
Jérémie Panzer
801dda912c Merge pull request #1714 from Athou/renovate/org.jsoup-jsoup-1.x
fix(deps): update dependency org.jsoup:jsoup to v1.19.1
2025-03-04 14:06:58 +01:00
renovate[bot]
a20005409a fix(deps): update dependency org.jsoup:jsoup to v1.19.1 2025-03-04 06:23:04 +00:00
renovate[bot]
6f1411d075 fix(deps): update mantine monorepo to ^7.17.1 (#1712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 06:22:47 +00:00
Athou
1aa263a6c0 simplify UnitOfWork code 2025-03-03 22:29:21 +01:00
Jérémie Panzer
9d511ac7dd Merge pull request #1710 from Athou/renovate/graalvm-setup-graalvm-digest
chore(deps): update graalvm/setup-graalvm digest to 01ed653
2025-03-03 21:24:20 +01:00
Athou
122e98cc76 revert to mantine 7.17.0 (#1711) 2025-03-03 21:21:44 +01:00
Athou
e445e5ea39 clarify in the README that if -Pnative is used, database should be specified (#1708) 2025-03-03 16:23:29 +01:00
renovate[bot]
5b9212015b chore(deps): update graalvm/setup-graalvm digest to 01ed653 2025-03-03 15:18:50 +00:00
Jérémie Panzer
293292f341 Merge pull request #1709 from Athou/renovate/graalvm-setup-graalvm-digest
chore(deps): update graalvm/setup-graalvm digest to 271a696
2025-03-03 16:18:21 +01:00
renovate[bot]
57d8a4dbb1 chore(deps): update graalvm/setup-graalvm digest to 271a696 2025-03-03 11:35:48 +00:00
renovate[bot]
e104f531f9 chore(deps): lock file maintenance 2025-03-03 02:42:48 +00:00
Athou
bf1361926f don't host by default 2025-03-02 19:21:07 +01:00
Athou
cc4f4d9eb4 remove dev:typescript, we don't need it anymore with the vite checker plugin 2025-03-02 19:20:24 +01:00
Athou
706bad26f1 use typesafe mocks 2025-03-02 19:18:51 +01:00
Athou
4ecefe6491 reduce tooltip delay for all tests 2025-03-02 19:18:27 +01:00
Athou
937e7353ce build regexp only once 2025-03-02 15:49:02 +01:00
Athou
1dcf76fc0a remove warning about missing index during tests 2025-03-02 15:49:02 +01:00
renovate[bot]
9d794dcad7 fix(deps): update dependency @fontsource/open-sans to ^5.2.5 2025-03-02 15:49:02 +01:00
Athou
d11b666755 remove vitest-mock-extended as vitest now exposes a vi.mocked() function 2025-03-02 15:49:02 +01:00
Athou
7a444e4861 add tests for ActionButton 2025-03-02 15:49:02 +01:00
Athou
5992795579 fix tooltips not showing up in mobile view 2025-03-02 15:49:02 +01:00
Athou
4441d76a7f Merge remote-tracking branch 'origin/renovate/patch-fontsource-monorepo' 2025-03-02 11:39:21 +01:00
Athou
c1305b56e3 insert jakarta imports where javax import were positioned 2025-03-02 11:37:06 +01:00
Athou
cc0440c029 enable quarkus compression (compression in dropwizard was enabled by default) 2025-03-02 09:45:10 +01:00
renovate[bot]
f65591c170 fix(deps): update dependency @fontsource/open-sans to ^5.2.1 2025-03-02 06:57:09 +00:00
Jérémie Panzer
9a32dce9d1 Merge pull request #1707 from Athou/renovate/fontsource-monorepo
fix(deps): update dependency @fontsource/open-sans to ^5.2.0
2025-03-01 19:59:07 +01:00
renovate[bot]
789bd3edae fix(deps): update dependency @fontsource/open-sans to ^5.2.0 2025-03-01 18:15:36 +00:00
renovate[bot]
256cd426d9 fix(deps): update mantine monorepo to ^7.17.1 2025-03-01 10:40:20 +00:00
Jérémie Panzer
58af2da105 Merge pull request #1706 from Athou/renovate/typescript-5.x
chore(deps): update dependency typescript to ^5.8.2
2025-02-28 21:02:58 +01:00
renovate[bot]
e0de397273 chore(deps): update dependency typescript to ^5.8.2 2025-02-28 19:33:23 +00:00
Jérémie Panzer
75cc3cf29c Merge pull request #1705 from Athou/renovate/docker-setup-qemu-action-digest
chore(deps): update docker/setup-qemu-action digest to 2910929
2025-02-28 17:11:02 +01:00
renovate[bot]
af60758e2a chore(deps): update docker/setup-qemu-action digest to 2910929 2025-02-28 14:32:17 +00:00
Jérémie Panzer
01180e95a2 Merge pull request #1704 from Athou/renovate/docker-setup-qemu-action-digest
chore(deps): update docker/setup-qemu-action digest to 5964de0
2025-02-26 21:15:41 +01:00
renovate[bot]
fa683ef7e1 chore(deps): update docker/setup-qemu-action digest to 5964de0 2025-02-26 19:20:27 +00:00
Jérémie Panzer
462d17a429 Merge pull request #1703 from Athou/renovate/docker-setup-buildx-action-digest
chore(deps): update docker/setup-buildx-action digest to b5ca514
2025-02-26 20:20:04 +01:00
Jérémie Panzer
17f71a40d4 Merge pull request #1702 from Athou/renovate/docker-build-push-action-digest
chore(deps): update docker/build-push-action digest to 471d1dc
2025-02-26 20:19:54 +01:00
renovate[bot]
de91a3a05a chore(deps): update docker/setup-buildx-action digest to b5ca514 2025-02-26 16:46:20 +00:00
renovate[bot]
ead587ee88 chore(deps): update docker/build-push-action digest to 471d1dc 2025-02-26 16:46:16 +00:00
Jérémie Panzer
62b3e6fb3a Merge pull request #1700 from Athou/renovate/actions-download-artifact-digest
chore(deps): update actions/download-artifact digest to cc20338
2025-02-26 17:46:01 +01:00
Jérémie Panzer
037ff15045 Merge pull request #1701 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.19.1 (minor)
2025-02-26 17:45:45 +01:00
renovate[bot]
ed35b06934 fix(deps): update quarkus.version to v3.19.1 2025-02-26 15:39:08 +00:00
renovate[bot]
3cfb1a13a7 chore(deps): update actions/download-artifact digest to cc20338 2025-02-26 15:38:48 +00:00
renovate[bot]
d04745d859 fix(deps): update dependency axios to ^1.8.1 2025-02-26 10:29:47 +00:00
Jérémie Panzer
58b18f36c5 Merge pull request #1699 from Athou/renovate/axios-1.x
fix(deps): update dependency axios to ^1.8.0
2025-02-26 08:10:25 +01:00
renovate[bot]
7282d18d8f fix(deps): update dependency axios to ^1.8.0 2025-02-26 06:46:03 +00:00
Jérémie Panzer
8e58fa22b4 Merge pull request #1698 from Athou/renovate/vite-6.x
chore(deps): update dependency vite to ^6.2.0
2025-02-25 05:09:34 +01:00
renovate[bot]
58d6eb2c5a chore(deps): update dependency vite to ^6.2.0 2025-02-25 03:36:32 +00:00
renovate[bot]
2f7c7498e2 chore(deps): update dependency vitest to ^3.0.7 2025-02-24 18:16:11 +00:00
renovate[bot]
bcf8dcd551 chore(deps): lock file maintenance 2025-02-24 05:39:25 +00:00
Jérémie Panzer
511f0a60bb Merge pull request #1697 from Athou/renovate/reduxjs-toolkit-2.x
fix(deps): update dependency @reduxjs/toolkit to ^2.6.0
2025-02-24 06:37:30 +01:00
renovate[bot]
72db0d815f fix(deps): update dependency @reduxjs/toolkit to ^2.6.0 2025-02-24 01:36:04 +00:00
renovate[bot]
280d0b7fdd chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.3 2025-02-23 19:49:59 +00:00
Athou
42e4575cb7 release 5.6.1 2025-02-23 20:49:01 +01:00
Jérémie Panzer
28a4bb403a Merge pull request #1691 from dcelasun/arch-package
Mention Arch package
2025-02-23 20:47:57 +01:00
Athou
cca3c907db documentation is now hosted on github pages 2025-02-23 20:23:02 +01:00
Athou
1a5b932742 upload generated documentation to github pages 2025-02-23 20:05:04 +01:00
Jérémie Panzer
a1d3f3008a Merge pull request #1693 from Athou/renovate/ncipollo-release-action-digest
chore(deps): update ncipollo/release-action digest to 440c8c1
2025-02-23 08:22:14 +01:00
Jérémie Panzer
902f2efbd2 Merge pull request #1694 from Athou/renovate/vitest-mock-extended-3.x
chore(deps): update dependency vitest-mock-extended to v3
2025-02-23 08:21:47 +01:00
renovate[bot]
2e534af146 chore(deps): update dependency vitest-mock-extended to v3 2025-02-22 17:50:06 +00:00
renovate[bot]
23ca30c3c2 chore(deps): update ncipollo/release-action digest to 440c8c1 2025-02-22 17:49:45 +00:00
Athou
517eedad00 Merge branch 'generated-properties' 2025-02-22 11:12:16 +01:00
Jérémie Panzer
216ea1fb42 Merge pull request #1692 from Athou/renovate/actions-upload-artifact-digest
chore(deps): update actions/upload-artifact digest to 4cec3d8
2025-02-22 11:11:34 +01:00
Athou
640d1a0ce3 add a tool to generate a properties file from configuration (#1691) 2025-02-22 08:03:01 +01:00
renovate[bot]
bba7425b5f chore(deps): update actions/upload-artifact digest to 4cec3d8 2025-02-21 22:15:42 +00:00
D. Can Celasun
7a1a49bfb4 Mention Arch package 2025-02-21 13:02:59 +00:00
Jérémie Panzer
e451e6698c Merge pull request #1690 from Athou/renovate/org.apache.maven.plugins-maven-compiler-plugin-3.x
chore(deps): update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.14.0
2025-02-21 13:52:18 +01:00
renovate[bot]
9af3f21404 chore(deps): update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.14.0 2025-02-21 09:32:56 +00:00
renovate[bot]
7b14a9c0c2 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.3 2025-02-21 02:12:51 +00:00
Jérémie Panzer
0b65cc9510 Merge pull request #1689 from Athou/renovate/vite-plugin-checker-0.x
chore(deps): update dependency vite-plugin-checker to ^0.9.0
2025-02-20 18:29:10 +01:00
renovate[bot]
7879ab9b61 chore(deps): update dependency vite-plugin-checker to ^0.9.0 2025-02-20 15:11:34 +00:00
Athou
e6bebcafb3 allow iframes in feed entries (#1688) 2025-02-20 10:07:45 +01:00
renovate[bot]
3b465cebb7 fix(deps): update quarkus.version to v3.18.4 2025-02-19 20:28:09 +00:00
renovate[bot]
aeb211be06 chore(deps): update dependency vite to ^6.1.1 2025-02-19 18:26:04 +00:00
Jérémie Panzer
ad992aea7b Merge pull request #1687 from Athou/renovate/docker-build-push-action-digest
chore(deps): update docker/build-push-action digest to 0adf995
2025-02-19 19:25:11 +01:00
renovate[bot]
d848f72a0b chore(deps): update docker/build-push-action digest to 0adf995 2025-02-19 17:00:18 +00:00
Athou
0db087908d remove "jakarta.ws.rs.WebApplicationException" from the errors displayed in the client 2025-02-19 13:08:43 +01:00
Jérémie Panzer
42138d04d6 Merge pull request #1685 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.2.0
2025-02-19 10:16:26 +01:00
renovate[bot]
4522a9d0d5 fix(deps): update dependency react-router-dom to ^7.2.0 2025-02-19 06:32:35 +00:00
Jérémie Panzer
7440fcad0e Merge pull request #1686 from Athou/renovate/react-icons-5.x
fix(deps): update dependency react-icons to ^5.5.0
2025-02-19 07:31:05 +01:00
renovate[bot]
fc51c1882f fix(deps): update dependency react-icons to ^5.5.0 2025-02-19 05:47:14 +00:00
renovate[bot]
e24498b31f chore(deps): update dependency vitest to ^3.0.6 2025-02-18 14:27:16 +00:00
Athou
60fdc79563 don't expose rome's FeedException 2025-02-18 08:57:24 +01:00
Athou
6729ebc6ea throw a specific exception if we can't parse the url's content (#1684) 2025-02-18 07:41:02 +01:00
Athou
c8ff216ce5 README update 2025-02-17 21:23:38 +01:00
Jérémie Panzer
98c4150cfe Merge pull request #1683 from Athou/renovate/mantine-monorepo
fix(deps): update mantine monorepo to ^7.17.0 (minor)
2025-02-17 21:11:53 +01:00
renovate[bot]
128332d710 fix(deps): update mantine monorepo to ^7.17.0 2025-02-17 19:31:07 +00:00
renovate[bot]
eabcb519a4 chore(deps): update react monorepo 2025-02-17 11:01:27 +00:00
renovate[bot]
5e14cead3d chore(deps): lock file maintenance 2025-02-17 01:44:54 +00:00
renovate[bot]
b601f938ff chore(deps): update ibm-semeru-runtimes docker tag to open-21.0.6_7-jre (#1682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-16 21:26:14 +00:00
renovate[bot]
4acfda32d0 chore(deps): update dependency @types/react to ^19.0.9 2025-02-16 13:16:31 +00:00
Athou
54da4e6839 release 5.6.0 2025-02-16 14:14:30 +01:00
Athou
3a6b4c588c PRs and renovate now build the docker images regardless of the branch/tag 2025-02-15 10:20:39 +01:00
Athou
48071b9fd1 PRs now build the docker images but don't push them 2025-02-15 10:18:16 +01:00
Athou
f519aa039f block local addresses to prevent SSRF attacks 2025-02-14 16:20:04 +01:00
Athou
dc3e5476a1 reload the tree when we receive a websocket notification about an unknown feed 2025-02-14 16:16:07 +01:00
Athou
903035ecfc formatting 2025-02-14 16:16:07 +01:00
Athou
13ad57da10 make sure the tree has been reloaded before navigating to the new feed subscription 2025-02-14 16:16:06 +01:00
Athou
44bc24c22a ubuntu-22.04-arm is supposed to be more stable 2025-02-14 16:16:06 +01:00
Athou
97f90405fc try to fix flaky IT test 2025-02-14 16:16:06 +01:00
renovate[bot]
0fc2a0b022 fix(deps): update dependency @monaco-editor/react to ^4.7.0 2025-02-13 19:39:46 +00:00
Jérémie Panzer
89eb641704 Merge pull request #1679 from Athou/renovate/graalvm-setup-graalvm-digest
chore(deps): update graalvm/setup-graalvm digest to b0cb26a
2025-02-13 06:22:29 +01:00
renovate[bot]
c53da9f631 chore(deps): update graalvm/setup-graalvm digest to b0cb26a 2025-02-12 22:32:09 +00:00
renovate[bot]
998868e63a fix(deps): update quarkus.version to v3.18.3 2025-02-12 18:26:21 +00:00
Athou
93f22d2351 reduce max interval to 4h 2025-02-12 18:17:39 +01:00
Athou
c3782bd7d2 also constrain to lower bound 2025-02-12 18:04:56 +01:00
Athou
f330349397 update documentation 2025-02-12 17:31:46 +01:00
Athou
99c973c8c2 change the default value of empirical interval calculation (#1677) 2025-02-12 17:26:38 +01:00
Athou
469420b5bf feed refresh engine previously hardcoded values are now configurable (#1677) 2025-02-12 17:08:20 +01:00
Athou
bde556d41f start to back off when we repeatedly receive a 429 2025-02-12 08:00:27 +01:00
Jérémie Panzer
bf6c2d7beb Merge pull request #1678 from Athou/renovate/node-22.x
chore(deps): update node.js to v22.14.0
2025-02-11 16:03:32 +01:00
renovate[bot]
fa62ca21e0 chore(deps): update node.js to v22.14.0 2025-02-11 11:47:44 +00:00
renovate[bot]
7dcf76da84 fix(deps): update dependency interweave to ^13.1.1 2025-02-10 21:40:32 +01:00
renovate[bot]
3dc80fa762 chore(deps): lock file maintenance 2025-02-10 01:47:04 +00:00
Athou
dbce12492b release 5.5.0 2025-02-09 16:31:06 +01:00
renovate[bot]
85f5eaffec fix(deps): update mantine monorepo to ^7.16.3 2025-02-09 06:12:54 +00:00
Athou
106276351e use React 19 features to be able to remove unmaintained React Helmet 2025-02-07 20:13:13 +01:00
Athou
961fb6a464 redoc upgrade 2025-02-07 19:42:39 +01:00
Jérémie Panzer
ac3d9ef57f Merge pull request #1675 from Athou/renovate/docker-setup-qemu-action-digest
chore(deps): update docker/setup-qemu-action digest to 4574d27
2025-02-06 19:55:54 +01:00
Jérémie Panzer
3478ee4815 Merge pull request #1674 from Athou/renovate/docker-setup-buildx-action-digest
chore(deps): update docker/setup-buildx-action digest to f7ce87c
2025-02-06 19:55:49 +01:00
renovate[bot]
3dc02d7ba1 chore(deps): update docker/setup-qemu-action digest to 4574d27 2025-02-06 16:38:04 +00:00
renovate[bot]
c886f8b83c chore(deps): update docker/setup-buildx-action digest to f7ce87c 2025-02-06 16:38:00 +00:00
renovate[bot]
4a2154d0b3 fix(deps): update quarkus.version to v3.18.2 2025-02-05 19:35:32 +00:00
Jérémie Panzer
ba530d5019 Merge pull request #1673 from Athou/renovate/vite-6.x
chore(deps): update dependency vite to ^6.1.0
2025-02-05 20:34:57 +01:00
renovate[bot]
85b6209c52 chore(deps): update dependency vite to ^6.1.0 2025-02-05 16:58:47 +00:00
Athou
7ff86a5e31 make audio enclosures fill available width 2025-02-05 16:51:23 +01:00
Athou
8edd6a1e2d correctly handle 0 as a Retry-Header value (#1671) 2025-02-05 07:50:10 +01:00
Jérémie Panzer
6e65ed49e9 Merge pull request #1670 from Athou/renovate/com.microsoft.playwright-playwright-1.x
chore(deps): update dependency com.microsoft.playwright:playwright to v1.50.0
2025-02-04 23:29:26 +01:00
renovate[bot]
711b01abfa chore(deps): update dependency com.microsoft.playwright:playwright to v1.50.0 2025-02-04 22:01:19 +00:00
renovate[bot]
c7014ca2a1 chore(deps): update dependency vitest to ^3.0.5 2025-02-03 16:31:55 +00:00
renovate[bot]
a3984cd959 chore(deps): lock file maintenance 2025-02-03 08:00:36 +00:00
Athou
8d85b1bcba tweak tests to be more resilient 2025-02-03 08:55:26 +01:00
Athou
c451eee406 fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.4.2
remove workaround that is no longer needed
2025-02-02 15:58:59 +01:00
Jérémie Panzer
8f42135996 Merge pull request #1669 from Athou/renovate/linguijs-monorepo
fix(deps): update linguijs monorepo to ^5.2.0 (minor)
2025-02-01 16:28:59 +01:00
renovate[bot]
2c26aeed17 fix(deps): update linguijs monorepo to ^5.2.0 2025-02-01 14:12:40 +00:00
renovate[bot]
e2c4aa998b fix(deps): update dependency react-router-dom to ^7.1.5 2025-02-01 13:20:46 +00:00
Athou
c9e3b7f349 renovate already builds on push, don't trigger twice when it opens a PR 2025-02-01 14:19:50 +01:00
Athou
ebb4e52ba7 don't use lingui before it's initialized 2025-02-01 12:32:44 +01:00
Jérémie Panzer
1ddfdfb12e Merge pull request #1666 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.18.1 (minor)
2025-01-31 07:47:00 +01:00
Jérémie Panzer
81f16aea62 Merge pull request #1667 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11.1.0
2025-01-31 07:25:16 +01:00
renovate[bot]
429ec193c8 fix(deps): update dependency react-router-dom to ^7.1.4 2025-01-30 18:19:26 +00:00
renovate[bot]
732b714448 chore(deps): update dependency npm to v11.1.0 2025-01-30 01:05:55 +00:00
renovate[bot]
82e0405ad9 fix(deps): update quarkus.version to v3.18.1 2025-01-29 20:55:23 +00:00
Athou
9ef002fcd1 swagger-ui-react is no longer used 2025-01-29 15:06:00 +01:00
Athou
ec938e416c README clarification 2025-01-28 10:32:04 +01:00
Athou
37cf711cbc add support for the Retry-After header sent by OpenRSS 2025-01-27 07:48:19 +01:00
renovate[bot]
de441e4ff7 chore(deps): lock file maintenance 2025-01-27 01:07:29 +00:00
renovate[bot]
46251526b6 fix(deps): update dependency @reduxjs/toolkit to ^2.5.1 2025-01-26 22:09:50 +00:00
Jérémie Panzer
67eeea0b06 Merge pull request #1664 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.21.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.2
2025-01-26 23:09:03 +01:00
renovate[bot]
b49ccc4cd9 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.2 2025-01-26 16:48:25 +00:00
renovate[bot]
8586a8b57b fix(deps): update mantine monorepo to ^7.16.2 2025-01-26 13:53:50 +00:00
Jérémie Panzer
d9f63786a8 Merge pull request #1663 from Athou/renovate/swagger-ui-react-4.x
chore(deps): update dependency @types/swagger-ui-react to ^4.19.0
2025-01-26 14:53:04 +01:00
renovate[bot]
8f0c8b68b9 chore(deps): update dependency @types/swagger-ui-react to ^4.19.0 2025-01-26 12:40:20 +00:00
Jérémie Panzer
15e574c5c4 Merge pull request #1662 from Athou/renovate/docker-build-push-action-digest
chore(deps): update docker/build-push-action digest to ca877d9
2025-01-24 11:21:15 +01:00
renovate[bot]
fe532242b4 chore(deps): update docker/build-push-action digest to ca877d9 2025-01-24 09:35:09 +00:00
renovate[bot]
fb48ff0858 chore(deps): update dependency vitest to ^3.0.4 2025-01-23 21:13:02 +00:00
Athou
8d850639d7 remove branches-ignore because it applies to target branch 2025-01-23 22:11:53 +01:00
Athou
ee73195915 add github actions permissions 2025-01-23 21:50:27 +01:00
Athou
72d9dad61b fix "an artifact with this name already exists on the workflow run" 2025-01-23 21:50:12 +01:00
Athou
fde8dab8cd simplify youtube channels url detection 2025-01-23 21:49:52 +01:00
Athou
dae5efa787 allow next Java LTS version 2025-01-23 21:49:28 +01:00
Jérémie Panzer
3c067140fd Merge pull request #1661 from Athou/renovate/patch-react-monorepo
chore(deps): update dependency @types/react to ^19.0.8
2025-01-23 18:39:30 +01:00
renovate[bot]
4ccbe81e87 chore(deps): update dependency @types/react to ^19.0.8 2025-01-23 13:31:56 +00:00
Jérémie Panzer
3d5d93bb72 Merge pull request #1660 from Athou/renovate/patch-quarkus.version
fix(deps): update quarkus.version to v3.17.8 (patch)
2025-01-22 21:55:17 +01:00
renovate[bot]
4138b6eb9b fix(deps): update quarkus.version to v3.17.8 2025-01-22 18:33:36 +00:00
Jérémie Panzer
9c39c95a9b Merge pull request #1659 from Athou/renovate/pin-dependencies
chore(deps): pin dependencies
2025-01-22 10:53:56 +01:00
Jérémie Panzer
32b2bf99a4 Merge pull request #1658 from Athou/renovate/node-22.13.x
chore(deps): update node.js to v22.13.1
2025-01-22 08:19:38 +01:00
renovate[bot]
cf459876af chore(deps): update node.js to v22.13.1 2025-01-22 06:53:04 +00:00
renovate[bot]
6698bd74b5 chore(deps): pin dependencies 2025-01-22 06:52:59 +00:00
Athou
c81d06e5f3 pin github actions 2025-01-22 07:52:09 +01:00
renovate[bot]
b12a78dc84 fix(deps): update dependency tss-react to ^4.9.15 2025-01-21 21:34:41 +00:00
renovate[bot]
b076587e44 chore(deps): update dependency vitest to ^3.0.3 2025-01-21 17:42:28 +00:00
renovate[bot]
bb12f16bea chore(deps): update dependency vite to ^6.0.11 2025-01-21 12:35:00 +00:00
renovate[bot]
e80caadd12 chore(deps): update dependency vite to ^6.0.10 2025-01-20 21:35:21 +00:00
renovate[bot]
846d93f2b2 chore(deps): lock file maintenance 2025-01-20 19:18:35 +00:00
Steven Conaway
0ed6f6ef9c chore(deps): move to react@^19 (#1657)
* chore(deps): move to react@^19

* chore(deps): manually override old peer dependencies

* chore(deps): upgrade rollup-plugin-visualizer

* chore(deps): remove `package-lock.json` and `node_modules/` and regen lockfile

* chore(deps): remove randomly added dependencies

* chore(deps): change override for react@^19 peer dep
2025-01-20 19:59:42 +01:00
renovate[bot]
15992dcb80 chore(deps): update dependency vite to ^6.0.9 2025-01-20 14:11:24 +00:00
renovate[bot]
1a5c399b54 chore(deps): lock file maintenance 2025-01-20 01:15:47 +00:00
Athou
5e92f9ffb8 we can skip the docker step altogether for PRs 2025-01-19 21:40:30 +01:00
renovate[bot]
71164d1b69 fix(deps): update mantine monorepo to ^7.16.1 2025-01-19 13:51:46 +00:00
renovate[bot]
6947670fe6 fix(deps): update dependency react-router-dom to ^7.1.3 2025-01-17 22:16:53 +00:00
renovate[bot]
30810e37b9 chore(deps): update dependency vitest to ^3.0.2 2025-01-17 21:52:28 +00:00
Athou
b17b2767b0 run CI on pull requests 2025-01-17 22:40:47 +01:00
Athou
d37cf5bbcf release 5.4.0 2025-01-17 17:15:09 +01:00
Athou
045baba705 use github actions to build a native arm64 docker image 2025-01-17 16:12:05 +01:00
renovate[bot]
3623dc8e1d chore(deps): update dependency vitest to ^3.0.1 2025-01-16 20:17:53 +00:00
renovate[bot]
2610c37067 fix(deps): update swagger.version to v2.2.28 2025-01-16 18:17:44 +00:00
renovate[bot]
286b69a646 fix(deps): update dependency react-router-dom to ^7.1.2 2025-01-16 15:56:44 +00:00
Jérémie Panzer
9673f27090 Merge pull request #1656 from Athou/renovate/major-vitest-monorepo
chore(deps): update dependency vitest to v3
2025-01-16 16:55:55 +01:00
renovate[bot]
0722599f6d chore(deps): update dependency vitest to v3 2025-01-16 14:17:05 +00:00
Jérémie Panzer
1df40d8370 Merge pull request #1655 from Athou/renovate/redoc-2.x
fix(deps): update dependency redoc to ^2.3.0
2025-01-16 15:15:28 +01:00
renovate[bot]
457e4ec69e fix(deps): update dependency redoc to ^2.3.0 2025-01-16 13:56:26 +00:00
renovate[bot]
647310a45f fix(deps): update quarkus.version to v3.17.7 2025-01-15 17:22:28 +00:00
renovate[bot]
e85c92f216 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.2 2025-01-15 02:04:18 +00:00
renovate[bot]
d93b0dbfd4 fix(deps): update dependency io.dropwizard.metrics:metrics-json to v4.2.30 2025-01-14 20:40:37 +00:00
Jérémie Panzer
b4e61ef547 Merge pull request #1654 from Athou/renovate/mantine-monorepo
fix(deps): update mantine monorepo to ^7.16.0 (minor)
2025-01-14 14:14:01 +01:00
Jérémie Panzer
71dffbba46 Merge pull request #1653 from Athou/renovate/debian-12.x
chore(deps): update debian docker tag to v12.9
2025-01-14 14:13:51 +01:00
renovate[bot]
2c0b0c4e3b fix(deps): update mantine monorepo to ^7.16.0 2025-01-14 12:49:52 +00:00
renovate[bot]
d868e58e1e chore(deps): update debian docker tag to v12.9 2025-01-14 12:49:31 +00:00
renovate[bot]
90eb2095bf chore(deps): lock file maintenance 2025-01-13 01:27:25 +00:00
Athou
62d3ed16e6 remove DOCTYPE declarations (#1260) 2025-01-10 16:18:49 +01:00
Jérémie Panzer
74f7c48818 Merge pull request #1652 from Athou/renovate/jsdom-26.x
chore(deps): update dependency jsdom to v26
2025-01-10 15:43:55 +01:00
renovate[bot]
23fe9c29ed chore(deps): update dependency jsdom to v26 2025-01-09 10:23:05 +00:00
renovate[bot]
8f7be8278a chore(deps): update dependency typescript to ^5.7.3 (#1651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 05:40:40 +00:00
renovate[bot]
49118b6ea0 fix(deps): update quarkus.version to v3.17.6 2025-01-08 17:50:38 +00:00
Jérémie Panzer
d97bd04ae2 Merge pull request #1649 from Athou/renovate/com.diffplug.spotless-spotless-maven-plugin-2.x
chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.1
2025-01-08 18:50:20 +01:00
Jérémie Panzer
8d11309b64 Merge pull request #1650 from Athou/renovate/node-22.x
chore(deps): update node.js to v22.13.0
2025-01-08 18:50:07 +01:00
renovate[bot]
68c24e4cb8 chore(deps): update node.js to v22.13.0 2025-01-08 17:21:57 +00:00
renovate[bot]
4e43e0235f chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.1 2025-01-08 15:35:52 +00:00
renovate[bot]
62b79a9625 fix(deps): update mantine monorepo to ^7.15.3 2025-01-08 15:35:49 +00:00
renovate[bot]
cb0706808c chore(deps): lock file maintenance 2025-01-06 02:01:57 +00:00
renovate[bot]
ffd5704b1e chore(deps): update dependency vite to ^6.0.7 2025-01-02 23:21:46 +00:00
Jérémie Panzer
3987077e5a Merge pull request #1647 from Athou/renovate/io.github.hakky54-sslcontext-kickstart-for-apache5-9.x
fix(deps): update dependency io.github.hakky54:sslcontext-kickstart-for-apache5 to v9
2025-01-01 16:02:38 +01:00
renovate[bot]
2e01a76784 fix(deps): update dependency io.github.hakky54:sslcontext-kickstart-for-apache5 to v9 2025-01-01 13:58:06 +00:00
Athou
8254093f5f fix tests failing because pubDate is older than a year 2024-12-30 08:07:03 +01:00
Jérémie Panzer
0b06526756 Merge pull request #1646 from Athou/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2024-12-30 07:20:00 +01:00
renovate[bot]
06731ae76d chore(deps): lock file maintenance 2024-12-30 00:18:52 +00:00
Jérémie Panzer
9a59453792 Merge pull request #1645 from Athou/renovate/patch-fontsource-monorepo
fix(deps): update dependency @fontsource/open-sans to ^5.1.1
2024-12-29 19:01:50 +01:00
renovate[bot]
c195a52c89 fix(deps): update dependency @fontsource/open-sans to ^5.1.1 2024-12-29 14:30:46 +00:00
Jérémie Panzer
3d7924f953 Merge pull request #1644 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.21.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.1
2024-12-29 10:20:33 +01:00
renovate[bot]
f29efd7fae chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.1 2024-12-28 15:55:17 +00:00
Jérémie Panzer
157bff3c83 Merge pull request #1643 from Athou/renovate/rollup-plugin-visualizer-5.13.x
chore(deps): update dependency rollup-plugin-visualizer to ^5.13.1
2024-12-28 13:41:58 +01:00
renovate[bot]
5c17bbc36d chore(deps): update dependency rollup-plugin-visualizer to ^5.13.1 2024-12-27 11:16:46 +00:00
Jérémie Panzer
c85e72e70c Merge pull request #1642 from Athou/renovate/rollup-plugin-visualizer-5.x
chore(deps): update dependency rollup-plugin-visualizer to ^5.13.0
2024-12-27 12:16:03 +01:00
renovate[bot]
01150f67e1 chore(deps): update dependency rollup-plugin-visualizer to ^5.13.0 2024-12-27 10:55:17 +00:00
Jérémie Panzer
75aca7aa6f Merge pull request #1638 from bestZwei/patch-1
Update zh/messages.po
2024-12-27 05:05:22 +01:00
zwei
affde7e43c Update messages.po
add a few Chinese Translations
2024-12-26 22:49:36 +08:00
renovate[bot]
b9b1b53235 chore(deps): update dependency vite to ^6.0.6 2024-12-26 05:23:50 +00:00
renovate[bot]
708ebb8abc fix(deps): update dependency react-router-dom to ^7.1.1 2024-12-23 18:42:33 +00:00
renovate[bot]
83e763df0a fix(deps): update mantine monorepo to ^7.15.2 2024-12-23 11:22:09 +00:00
Jérémie Panzer
0ff812c1ea Merge pull request #1637 from Athou/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2024-12-23 12:21:17 +01:00
renovate[bot]
3e9dd6d8e2 chore(deps): lock file maintenance 2024-12-23 10:39:01 +00:00
Jérémie Panzer
23af73e847 Merge pull request #1626 from Athou/renovate/mantine-monorepo
fix(deps): update mantine monorepo (minor)
2024-12-23 11:37:18 +01:00
renovate[bot]
e79e4719fd fix(deps): update mantine monorepo 2024-12-23 10:17:49 +00:00
Jérémie Panzer
23fef98432 Merge pull request #1636 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.1.0
2024-12-21 10:50:32 +01:00
renovate[bot]
22478252e7 fix(deps): update dependency react-router-dom to ^7.1.0 2024-12-21 02:23:37 +00:00
Jérémie Panzer
76b1f3cd35 Merge pull request #1635 from Athou/renovate/vite-6.0.x
chore(deps): update dependency vite to ^6.0.5
2024-12-20 22:47:21 +01:00
renovate[bot]
420d73ec6a chore(deps): update dependency vite to ^6.0.5 2024-12-20 11:53:07 +00:00
renovate[bot]
e0211cfa0c chore(deps): update dependency @types/react to ^18.3.18 2024-12-20 02:18:48 +00:00
renovate[bot]
25a92c651c fix(deps): update quarkus.version to v3.17.5 2024-12-19 20:26:08 +00:00
renovate[bot]
0781205c69 chore(deps): update dependency vite to ^6.0.4 2024-12-19 11:15:48 +00:00
Jérémie Panzer
5102dd5e30 Merge pull request #1613 from Athou/renovate/vite-6.x
chore(deps): update dependency vite to v6
2024-12-16 21:30:14 +01:00
renovate[bot]
6ccfc3fd67 chore(deps): update dependency vite to v6 2024-12-16 20:14:47 +00:00
Athou
2791ed91ab lingui update 2024-12-16 21:11:54 +01:00
Jérémie Panzer
f40c198233 Merge pull request #1634 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11
2024-12-16 20:57:57 +01:00
renovate[bot]
003dc63121 chore(deps): update dependency npm to v11 2024-12-16 19:36:39 +00:00
renovate[bot]
f8ef1e2a99 chore(deps): update dependency @types/react to ^18.3.17 2024-12-16 15:42:17 +00:00
renovate[bot]
14c7078940 fix(deps): update querydsl.version to v6.10.1 2024-12-16 00:30:43 +00:00
Jérémie Panzer
074836d3e8 Merge pull request #1632 from Athou/renovate/querydsl.version
fix(deps): update querydsl.version to v6.10 (minor)
2024-12-14 06:59:55 +01:00
renovate[bot]
0cdbc144b3 fix(deps): update querydsl.version to v6.10 2024-12-13 20:09:27 +00:00
Jérémie Panzer
dc63ec24c0 Merge pull request #1630 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.0
2024-12-12 22:42:41 +01:00
renovate[bot]
6d4c6c36a5 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.21.0 2024-12-12 20:31:27 +00:00
renovate[bot]
464af5f4d9 fix(deps): update swagger.version to v2.2.27 2024-12-11 18:13:04 +00:00
renovate[bot]
aa94a46a3d fix(deps): update quarkus.version to v3.17.4 2024-12-11 16:27:30 +00:00
renovate[bot]
8542197dc3 chore(deps): update react monorepo 2024-12-11 06:15:47 +00:00
Jérémie Panzer
64d77eaef4 Merge pull request #1628 from Athou/renovate/reduxjs-toolkit-2.x
fix(deps): update dependency @reduxjs/toolkit to ^2.5.0
2024-12-11 07:14:49 +01:00
Jérémie Panzer
675ef8794c Merge pull request #1627 from Athou/renovate/react-redux-9.x
fix(deps): update dependency react-redux to ^9.2.0
2024-12-11 07:14:17 +01:00
renovate[bot]
4bcdbeb516 fix(deps): update dependency @reduxjs/toolkit to ^2.5.0 2024-12-11 04:59:19 +00:00
renovate[bot]
a9f37739fb fix(deps): update dependency react-redux to ^9.2.0 2024-12-11 02:11:18 +00:00
renovate[bot]
5ab0fc19da chore(deps): update dependency @types/react-dom to ^18.3.3 2024-12-09 22:54:32 +00:00
renovate[bot]
7b232425f3 fix(deps): update dependency monaco-editor to ^0.52.2 2024-12-09 19:06:36 +00:00
Jérémie Panzer
c0e7668140 Merge pull request #1625 from Athou/renovate/emotion-monorepo
fix(deps): update dependency @emotion/react to ^11.14.0
2024-12-09 16:02:32 +01:00
renovate[bot]
ae3f059257 fix(deps): update dependency @emotion/react to ^11.14.0 2024-12-09 12:04:31 +00:00
renovate[bot]
d44c7c1e95 fix(deps): update dependency tss-react to ^4.9.14 2024-12-09 07:48:44 +00:00
renovate[bot]
6cd9d134cf chore(deps): update dependency vite-tsconfig-paths to ^5.1.4 2024-12-07 05:53:17 +00:00
renovate[bot]
6f21ba8afc chore(deps): update react monorepo 2024-12-05 19:55:23 +00:00
renovate[bot]
b2fe13c117 chore(deps): update dependency npm to v10.9.2 2024-12-05 01:06:03 +00:00
renovate[bot]
03ece7a262 chore(deps): update dependency @types/react to ^18.3.13 (#1622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 01:05:43 +00:00
renovate[bot]
697fde2d0e chore(deps): update quarkus.version to v3.17.3 2024-12-04 20:09:33 +00:00
renovate[bot]
7f0f85b356 fix(deps): update dependency axios to ^1.7.9 2024-12-04 09:43:27 +00:00
Jérémie Panzer
a7d41debfe Merge pull request #1621 from Athou/renovate/node-22.x
chore(deps): update node.js to v22.12.0
2024-12-04 07:21:31 +01:00
renovate[bot]
57bf758108 chore(deps): update node.js to v22.12.0 2024-12-03 21:03:12 +00:00
Jérémie Panzer
b37d933047 Merge pull request #1620 from Athou/renovate/react-icons-5.x
fix(deps): update dependency react-icons to ^5.4.0
2024-12-03 13:08:38 +01:00
renovate[bot]
80ffef4555 fix(deps): update dependency react-icons to ^5.4.0 2024-12-03 11:31:44 +00:00
renovate[bot]
af5a0002aa fix(deps): update dependency react-router-dom to ^7.0.2 2024-12-03 04:09:43 +00:00
Athou
cd24e412e3 release 5.3.6 2024-12-02 18:50:14 +01:00
Athou
a073d843ab ignore invalid cache control values (#1619) 2024-12-02 18:43:46 +01:00
renovate[bot]
8ccb59ed18 chore(deps): update dependency vitest to ^2.1.8 2024-12-02 14:56:45 +00:00
renovate[bot]
e6dc7d2d0d chore(deps): lock file maintenance 2024-12-02 03:59:25 +00:00
Athou
f7b21ca3f6 release 5.3.5 2024-12-02 04:57:26 +01:00
Athou
df3a1bcdb6 update changelog 2024-12-02 04:54:58 +01:00
renovate[bot]
5bec494a7c fix(deps): update dependency org.jsoup:jsoup to v1.18.3 2024-12-02 03:18:47 +00:00
renovate[bot]
d8eef4dd9f chore(deps): lock file maintenance 2024-12-02 02:00:17 +00:00
renovate[bot]
d80138caf3 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.20.2 2024-12-01 06:04:51 +00:00
Athou
d26c103aa5 migrate to markdown documentation 2024-12-01 06:04:08 +01:00
Athou
249231f57e add support for JDK 23 compilation (#1618) 2024-12-01 05:43:35 +01:00
Athou
7a838cddad honor Cache-Control response header (#1615) 2024-11-29 17:06:57 +01:00
renovate[bot]
477f2cd6db fix(deps): update dependency io.quarkus.platform:quarkus-bom to v3.17.2 2024-11-29 13:10:54 +00:00
Athou
9915f05f73 remove lingui warnings 2024-11-29 14:08:25 +01:00
Athou
0a16bb2fba if width is specified, keep images aspect ratio (#1595) 2024-11-29 13:30:48 +01:00
Jérémie Panzer
3d4faf2406 Merge pull request #1616 from Athou/renovate/major-linguijs-monorepo
chore(deps): update linguijs monorepo to v5 (major)
2024-11-29 07:42:28 +01:00
Jérémie Panzer
63de6fe833 Merge pull request #1617 from Athou/renovate/reduxjs-toolkit-2.x
fix(deps): update dependency @reduxjs/toolkit to ^2.4.0
2024-11-29 07:42:17 +01:00
renovate[bot]
9c6219a58a fix(deps): update dependency @reduxjs/toolkit to ^2.4.0 2024-11-28 22:00:49 +00:00
renovate[bot]
3e664d4287 chore(deps): update linguijs monorepo to v5 2024-11-28 19:15:26 +00:00
renovate[bot]
4c4ffd84f3 chore(deps): update linguijs monorepo to ^4.14.1 2024-11-28 15:13:09 +00:00
renovate[bot]
f555f0e392 fix(deps): update mantine monorepo to ^7.14.3 2024-11-28 14:43:41 +00:00
renovate[bot]
124166738b fix(deps): update dependency io.dropwizard.metrics:metrics-json to v4.2.29 2024-11-28 09:55:36 +00:00
Jérémie Panzer
8b32dcc576 Merge pull request #1614 from Athou/renovate/quarkus.version
fix(deps): update dependency io.quarkus.platform:quarkus-bom to v3.17.0
2024-11-27 16:13:19 +01:00
renovate[bot]
105651215a fix(deps): update dependency io.quarkus.platform:quarkus-bom to v3.17.0 2024-11-27 14:52:32 +00:00
renovate[bot]
d9c6cbd072 fix(deps): update dependency org.jsoup:jsoup to v1.18.2 2024-11-27 13:00:42 +00:00
renovate[bot]
b4c52e06fe chore(deps): update dependency vitest to ^2.1.6 2024-11-27 11:35:05 +00:00
Athou
2565dfe528 remove workaround because quarkus.http.auth.form.cookie-max-age is now available 2024-11-27 10:05:26 +01:00
Athou
b5036c9148 Merge branch 'renovate/quarkus.version' 2024-11-27 10:00:53 +01:00
renovate[bot]
e2c8ddb8f7 chore(deps): update quarkus.version to v3.16.4 2024-11-27 10:00:32 +01:00
renovate[bot]
85fbd284fa chore(deps): update dependency @vitejs/plugin-react to ^4.3.4 2024-11-26 11:15:33 +00:00
renovate[bot]
559fb69a64 fix(deps): update dependency axios to ^1.7.8 2024-11-25 23:25:26 +00:00
renovate[bot]
054c76716a chore(deps): lock file maintenance 2024-11-25 00:49:27 +00:00
renovate[bot]
ba17c9350f fix(deps): update mantine monorepo to ^7.14.2 2024-11-24 14:33:06 +00:00
Jérémie Panzer
781015eea4 Merge pull request #1608 from Athou/renovate/major-react-router-monorepo
fix(deps): update dependency react-router-dom to v7
2024-11-23 15:40:57 +01:00
Jérémie Panzer
13e5c0e8d2 Merge pull request #1607 from Athou/renovate/com.microsoft.playwright-playwright-1.x
chore(deps): update dependency com.microsoft.playwright:playwright to v1.49.0
2024-11-23 15:40:41 +01:00
Jérémie Panzer
4d88a30848 Merge pull request #1597 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.20.1
2024-11-23 15:40:05 +01:00
Jérémie Panzer
19c03de9e4 Merge pull request #1604 from Athou/renovate/debian-12.x
chore(deps): update debian docker tag to v12.8
2024-11-23 15:39:57 +01:00
Jérémie Panzer
e3169c9f2d Merge pull request #1609 from Athou/renovate/querydsl.version
fix(deps): update querydsl.version to v6.9 (minor)
2024-11-23 15:39:46 +01:00
Jérémie Panzer
e90fb0b56f Merge pull request #1611 from Athou/renovate/typescript-5.x
chore(deps): update dependency typescript to ^5.7.2
2024-11-23 15:39:31 +01:00
renovate[bot]
69607a5122 chore(deps): update dependency typescript to ^5.7.2 2024-11-22 19:13:41 +00:00
renovate[bot]
21eb8e6d9f fix(deps): update dependency react-router-dom to v7 2024-11-22 16:56:53 +00:00
renovate[bot]
a28a6b9dc4 fix(deps): update dependency io.github.openfeign.querydsl:querydsl-jpa to v6.9 2024-11-22 14:10:54 +00:00
renovate[bot]
a9cadbafeb chore(deps): update dependency com.microsoft.playwright:playwright to v1.49.0 2024-11-22 03:47:15 +00:00
renovate[bot]
d491af5a8d chore(deps): update dependency npm to v10.9.1 2024-11-22 00:38:41 +00:00
renovate[bot]
39c7934fb8 fix(deps): update dependency @emotion/react to ^11.13.5 2024-11-20 10:41:11 +00:00
renovate[bot]
76eba8cc63 chore(deps): update dependency vite-tsconfig-paths to ^5.1.3 2024-11-19 23:22:57 +00:00
renovate[bot]
7549ff2491 chore(deps): update dependency @types/tinycon to ^0.6.7 2024-11-19 18:55:08 +00:00
renovate[bot]
66cd18bc4b chore(deps): update dependency io.swagger.core.v3:swagger-maven-plugin-jakarta to v2.2.26 2024-11-19 06:34:58 +00:00
renovate[bot]
44d7a2c340 chore(deps): update dependency io.quarkus.platform:quarkus-maven-plugin to v3.15.2 2024-11-19 02:12:42 +00:00
renovate[bot]
d6ee63a01f chore(deps): update ibm-semeru-runtimes docker tag to open-21.0.5_11-jre 2024-11-18 22:02:31 +00:00
renovate[bot]
a495c9cacd fix(deps): update mantine monorepo to ^7.14.1 2024-11-18 04:24:54 +00:00
renovate[bot]
530185d15c chore(deps): lock file maintenance 2024-11-18 01:29:43 +00:00
renovate[bot]
8dd28c25a7 fix(deps): update dependency org.projectlombok:lombok to v1.18.36 2024-11-15 22:09:25 +00:00
renovate[bot]
50884b236c chore(deps): update dependency vitest to ^2.1.5 2024-11-13 16:06:06 +00:00
renovate[bot]
da9fe09e58 chore(deps): update dependency @types/tinycon to ^0.6.6 2024-11-12 09:54:28 +00:00
renovate[bot]
3c24c9aa7a chore(deps): update debian docker tag to v12.8 2024-11-12 02:04:08 +00:00
renovate[bot]
9d10a4b46f fix(deps): update dependency react-router-dom to ^6.28.0 2024-11-12 02:04:06 +00:00
renovate[bot]
d56ed3bd06 chore(deps): update linguijs monorepo to ^4.14.0 2024-11-11 21:19:20 +00:00
renovate[bot]
41c1c429d0 chore(deps): update dependency vite-tsconfig-paths to ^5.1.2 2024-11-11 19:43:20 +00:00
renovate[bot]
1a3d890b40 chore(deps): lock file maintenance (#1603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 19:42:44 +00:00
renovate[bot]
e3ec9b2ccd chore(deps): update dependency vite to ^5.4.11 2024-11-11 13:43:13 +00:00
renovate[bot]
f69878a242 fix(deps): update mantine monorepo to ^7.13.5 2024-11-09 02:31:03 +00:00
renovate[bot]
ea766706fb chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.20.1 2024-11-07 19:31:05 +00:00
renovate[bot]
306507af80 fix(deps): update dependency react-swipeable to ^7.0.2 2024-11-04 17:47:41 +00:00
renovate[bot]
67084783b2 chore(deps): lock file maintenance 2024-11-04 00:09:27 +00:00
renovate[bot]
7cbb75f717 chore(deps): update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.2 2024-11-02 21:48:13 +00:00
renovate[bot]
1c335492d5 chore(deps): update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.2 2024-11-02 20:25:15 +00:00
Jérémie Panzer
de92e74c8e Merge pull request #1594 from Athou/renovate/node-22.x
chore(deps): update dependency node to v22
2024-10-29 18:07:01 +01:00
renovate[bot]
9cbbd30618 chore(deps): update dependency node to v22 2024-10-29 16:43:49 +00:00
Jérémie Panzer
f14f1493c4 Merge pull request #1593 from Athou/renovate/patch-vitest-monorepo
chore(deps): update dependency vitest to ^2.1.4
2024-10-29 00:09:00 +01:00
renovate[bot]
e68c8fdbe1 chore(deps): update dependency vitest to ^2.1.4 2024-10-28 22:13:08 +00:00
renovate[bot]
e094972aa2 fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.4.1 2024-10-28 19:56:15 +00:00
renovate[bot]
ff831c6d2b chore(deps): lock file maintenance 2024-10-28 01:19:01 +00:00
Jérémie Panzer
9957cda11a Merge pull request #1592 from Athou/renovate/com.puppycrawl.tools-checkstyle-10.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.19.0
2024-10-26 16:33:49 +02:00
renovate[bot]
6809822000 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.19.0 2024-10-26 13:58:03 +00:00
Jérémie Panzer
8048b1a9aa Merge pull request #1591 from Athou/renovate/org.apache.maven.plugins-maven-checkstyle-plugin-3.x
chore(deps): update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.6.0
2024-10-26 05:57:45 +02:00
renovate[bot]
8557bd018a chore(deps): update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.6.0 2024-10-25 18:25:58 +00:00
Jérémie Panzer
183d5fd162 Merge pull request #1590 from Athou/renovate/com.ibm.icu-icu4j-76.x
fix(deps): update dependency com.ibm.icu:icu4j to v76
2024-10-25 17:22:54 +02:00
renovate[bot]
411f86fbeb fix(deps): update dependency com.ibm.icu:icu4j to v76 2024-10-25 06:17:19 +00:00
renovate[bot]
5493046f25 fix(deps): update mantine monorepo to ^7.13.4 2024-10-23 18:02:39 +00:00
Athou
42015015a5 release 5.3.4 2024-10-23 20:01:40 +02:00
Athou
f3d2808f7d add support for Internationalized Domain Names (#1588) 2024-10-23 19:58:47 +02:00
renovate[bot]
906c353a7f chore(deps): update dependency @types/react to ^18.3.12 2024-10-23 06:05:51 +00:00
renovate[bot]
93dea83cd3 chore(deps): update dependency vite to ^5.4.10 2024-10-23 03:47:21 +00:00
renovate[bot]
1fc76ce1ad chore(deps): update dependency org.apache.maven.plugins:maven-help-plugin to v3.5.1 2024-10-21 19:53:59 +00:00
Athou
a337b01bc7 release 5.3.3 2024-10-21 21:53:28 +02:00
Athou
689e5c0004 remove image bottom margins (#1587) 2024-10-21 21:51:46 +02:00
Athou
d5a3c81c85 release 5.3.2 2024-10-21 21:20:27 +02:00
Athou
8230fde5d2 preserve style attribute from images (#1587) 2024-10-21 21:15:57 +02:00
renovate[bot]
b35513ea84 chore(deps): lock file maintenance 2024-10-21 01:29:16 +00:00
renovate[bot]
42a7785ca1 chore(deps): update dependency @vitejs/plugin-react to ^4.3.3 2024-10-19 18:29:57 +00:00
Jérémie Panzer
ea5ee4f04f Merge pull request #1586 from Athou/renovate/com.microsoft.playwright-playwright-1.x
chore(deps): update dependency com.microsoft.playwright:playwright to v1.48.0
2024-10-18 05:53:42 +02:00
renovate[bot]
3e14b12d4f chore(deps): update dependency com.microsoft.playwright:playwright to v1.48.0 2024-10-17 21:46:18 +00:00
renovate[bot]
78cc30f828 chore(deps): update dependency @biomejs/biome to v1.9.4 2024-10-17 21:46:15 +00:00
renovate[bot]
6091c84e60 fix(deps): update mantine monorepo to ^7.13.3 2024-10-17 09:14:54 +00:00
Jérémie Panzer
6ea95ad254 Merge pull request #1585 from Athou/renovate/redoc-2.x
fix(deps): update dependency redoc to ^2.2.0
2024-10-16 14:24:28 +02:00
renovate[bot]
7f888d926e fix(deps): update dependency redoc to ^2.2.0 2024-10-16 11:00:17 +00:00
Jérémie Panzer
5e4e02474f Merge pull request #1583 from Athou/renovate/linguijs-monorepo
chore(deps): update linguijs monorepo to ^4.13.0 (minor)
2024-10-15 16:07:30 +02:00
renovate[bot]
bff8611b42 chore(deps): update linguijs monorepo to ^4.13.0 2024-10-15 10:56:26 +00:00
renovate[bot]
f674048af3 fix(deps): update dependency org.passay:passay to v1.6.6 2024-10-15 02:21:46 +00:00
Jérémie Panzer
0265c24cf9 Merge pull request #1582 from Athou/renovate/reduxjs-toolkit-2.x
fix(deps): update dependency @reduxjs/toolkit to ^2.3.0
2024-10-15 04:21:26 +02:00
renovate[bot]
f8c3a229ec fix(deps): update dependency @reduxjs/toolkit to ^2.3.0 2024-10-14 21:53:07 +00:00
renovate[bot]
c424f40420 chore(deps): update dependency vitest to ^2.1.3 2024-10-14 21:52:47 +00:00
renovate[bot]
b77666cfe5 chore(deps): update dependency vite to ^5.4.9 2024-10-14 19:06:28 +00:00
renovate[bot]
193d1604d9 chore(deps): lock file maintenance 2024-10-14 03:20:28 +00:00
renovate[bot]
4efc6296b5 chore(deps): lock file maintenance 2024-10-14 01:22:19 +00:00
Jérémie Panzer
f753a4bdda Merge pull request #1578 from Athou/renovate/react-router-monorepo
fix(deps): update dependency react-router-dom to ^6.27.0
2024-10-12 07:25:02 +02:00
Jérémie Panzer
afaaaf9657 Merge pull request #1577 from Athou/renovate/linguijs-monorepo
chore(deps): update linguijs monorepo to ^4.12.0 (minor)
2024-10-12 07:24:54 +02:00
renovate[bot]
4d46896bf0 fix(deps): update dependency react-router-dom to ^6.27.0 2024-10-11 20:09:41 +00:00
renovate[bot]
2ad28c927f chore(deps): update linguijs monorepo to ^4.12.0 2024-10-11 15:36:55 +00:00
renovate[bot]
b9680a66ef chore(deps): update dependency @types/react-dom to ^18.3.1 2024-10-11 15:36:38 +00:00
Athou
4f687d5857 indicate that -Xmx also works for the native package (#1539) 2024-10-10 15:29:18 +02:00
renovate[bot]
9cca026834 chore(deps): update dependency typescript to ^5.6.3 2024-10-09 01:04:51 +00:00
renovate[bot]
058a9cd192 fix(deps): update dependency @reduxjs/toolkit to ^2.2.8 2024-10-08 07:21:11 +00:00
renovate[bot]
57d2ede86e chore(deps): update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.1 2024-10-07 13:07:50 +00:00
renovate[bot]
e3abea4ec5 chore(deps): lock file maintenance 2024-10-07 00:38:09 +00:00
renovate[bot]
b831f1f35c chore(deps): update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.1 2024-10-06 12:53:45 +00:00
Athou
74bce1308c release 5.3.1 2024-10-04 20:33:56 +02:00
Athou
98cfa6d2c8 add regression test 2024-10-04 20:24:43 +02:00
Athou
99a02a2186 fix issue with some HTTP feeds (#1572) 2024-10-04 20:20:02 +02:00
Jérémie Panzer
3431a813b1 Merge pull request #1574 from Athou/renovate/npm-10.x
chore(deps): update dependency npm to v10.9.0
2024-10-04 07:51:15 +02:00
renovate[bot]
e9e0e8d32b chore(deps): update dependency npm to v10.9.0 2024-10-03 19:31:06 +00:00
renovate[bot]
2d14409d35 fix(deps): update dependency io.dropwizard.metrics:metrics-json to v4.2.28 2024-10-03 19:31:04 +00:00
Jérémie Panzer
a8200e5c58 Merge pull request #1573 from Athou/renovate/node-20.x
chore(deps): update dependency node to v20.18.0
2024-10-03 21:30:44 +02:00
renovate[bot]
79a8df8b06 chore(deps): update dependency node to v20.18.0 2024-10-03 19:00:34 +00:00
renovate[bot]
061a5f0262 fix(deps): update mantine monorepo to ^7.13.2 2024-10-03 13:32:54 +00:00
renovate[bot]
821bdb3b0f chore(deps): update dependency vitest to ^2.1.2 2024-10-02 21:46:25 +00:00
renovate[bot]
606dfa9299 chore(deps): update dependency @types/react to ^18.3.11 2024-10-02 18:42:46 +00:00
renovate[bot]
131357c616 fix(deps): update swagger.version to v2.2.25 2024-10-02 13:17:53 +00:00
renovate[bot]
f6d3493bad chore(deps): update dependency @biomejs/biome to v1.9.3 2024-10-01 14:28:45 +00:00
renovate[bot]
0c6104e25b fix(deps): update mantine monorepo to ^7.13.1 2024-09-30 09:40:08 +00:00
Jérémie Panzer
d73735a35d Merge pull request #1571 from Athou/renovate/vitejs-plugin-react-4.3.x
chore(deps): update dependency @vitejs/plugin-react to ^4.3.2
2024-09-30 06:31:07 +02:00
renovate[bot]
e725d2d6b6 chore(deps): update dependency @vitejs/plugin-react to ^4.3.2 2024-09-30 04:02:46 +00:00
renovate[bot]
f0e1279d68 chore(deps): lock file maintenance 2024-09-30 00:36:34 +00:00
renovate[bot]
c74c74d2c4 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v10.18.2 2024-09-29 19:11:52 +00:00
renovate[bot]
aa70cf5dcd chore(deps): update dependency @types/react to ^18.3.10 2024-09-27 16:41:55 +00:00
Jérémie Panzer
1055259627 Merge pull request #1569 from canoine/patch-1
Update fr/messages.po
2024-09-26 22:35:33 +02:00
canoine
302d37b6ef Update fr/messages.po
French translation update
2024-09-26 21:41:37 +02:00
Jérémie Panzer
8532a73d94 Merge pull request #1565 from Athou/renovate/quarkus.version
chore(deps): update quarkus.version to v3.15.1 (minor)
2024-09-26 10:25:09 +02:00
Athou
ffafb272cb update docs 2024-09-26 10:08:52 +02:00
Jérémie Panzer
22e0171a34 Merge pull request #1566 from dai/master
chore(ja): translated some strings
2024-09-26 10:03:32 +02:00
renovate[bot]
2b410f040c Update quarkus.version to v3.15.1 2024-09-26 08:02:28 +00:00
dai
259e8ad4e5 chore: translated some strings
Chore: Translated some new strings and reworked some wording.
2024-09-26 14:25:19 +09:00
Jérémie Panzer
21244dd9f5 Merge pull request #1564 from Athou/renovate/mantine-monorepo
Update mantine monorepo to ^7.13.0 (minor)
2024-09-25 12:52:53 +02:00
renovate[bot]
bc6206180d Update mantine monorepo to ^7.13.0 2024-09-25 09:53:00 +00:00
renovate[bot]
6e22d21358 Update dependency vite to ^5.4.8 2024-09-25 05:04:17 +00:00
renovate[bot]
95bdb4e700 Update dependency @types/react to ^18.3.9 2024-09-24 15:24:13 +00:00
Athou
9b7dbc68ab release 5.3.0 2024-09-24 07:58:43 +02:00
Athou
dc86c9b0db also manually load more entries if needed when pressing the next entry button in the header (#1557) 2024-09-24 07:54:50 +02:00
renovate[bot]
cb92ed753f Update swagger.version to v2.2.24 2024-09-23 16:36:50 +00:00
renovate[bot]
10a085e24e Lock file maintenance 2024-09-23 00:08:26 +00:00
renovate[bot]
3400a39edf Update dependency jsdom to ^25.0.1 2024-09-22 07:08:58 +00:00
Athou
21efffa345 Update dependency io.github.hakky54:sslcontext-kickstart-for-apache5 to v8.3.7
Update dependency org.apache.httpcomponents.client5:httpclient5 to v5.4
2024-09-22 09:07:20 +02:00
renovate[bot]
e2e80ba7e5 Update dependency com.github.eirslett:frontend-maven-plugin to v1.15.1 2024-09-21 18:11:14 +00:00
Jérémie Panzer
d988dba66e Merge pull request #1563 from Athou/renovate/querydsl.version
Update querydsl.version to v6.8 (minor)
2024-09-21 15:42:21 +02:00
renovate[bot]
403201fbff Update querydsl.version to v6.8 2024-09-21 13:25:28 +00:00
Athou
3cc93b51bb set default cooldown duration to 0 so it's not a breaking change 2024-09-21 10:57:16 +02:00
Athou
6a7d83bb45 show an error if force fetching feeds is not yet available 2024-09-21 10:31:17 +02:00
Athou
19c8db8b31 add a cooldown on the force refresh action (#1556) 2024-09-21 08:24:14 +02:00
renovate[bot]
0d75688ec8 Update dependency vite to ^5.4.7 2024-09-20 16:39:11 +00:00
renovate[bot]
e01dcb2f5b Update dependency @types/react to ^18.3.8 2024-09-19 22:25:45 +00:00
Jérémie Panzer
57757e2c14 Merge pull request #1561 from Athou/renovate/monaco-editor-0.x
Update dependency monaco-editor to ^0.52.0
2024-09-19 17:59:58 +02:00
renovate[bot]
779cd2fcfe Update dependency monaco-editor to ^0.52.0 2024-09-19 13:51:05 +00:00
renovate[bot]
94919f22e4 Update dependency @biomejs/biome to v1.9.2 2024-09-19 13:51:00 +00:00
Athou
d5cf690703 release 5.2.0 2024-09-18 17:21:10 +02:00
Athou
191574dace manually load more entries if needed when pressing a keyboard shortcut to go to the next entry (#1557) 2024-09-18 16:23:53 +02:00
renovate[bot]
ee7c6792c9 Update dependency @types/react to ^18.3.7 2024-09-17 11:39:38 +00:00
renovate[bot]
e2962dc2eb Update dependency vite to ^5.4.6 2024-09-16 23:19:58 +00:00
Jérémie Panzer
8c335cb8fd Merge pull request #1555 from victorhck/master
Update messages.po
2024-09-16 18:40:24 +02:00
Victorhck
461c18a591 Update messages.po
Keep in spanish the same name for Shift Key
2024-09-16 16:59:56 +02:00
Jérémie Panzer
363837ab26 we no longer need this file thanks to quarkus dev services 2024-09-16 15:55:06 +02:00
renovate[bot]
a184485421 Update dependency @types/react to ^18.3.6 2024-09-16 11:44:00 +00:00
renovate[bot]
f992c3f1a6 Lock file maintenance 2024-09-16 01:01:23 +00:00
Jérémie Panzer
3219a9e313 Merge pull request #1554 from Athou/renovate/biomejs-biome-1.9.x
Update dependency @biomejs/biome to v1.9.1
2024-09-15 21:39:44 +02:00
renovate[bot]
4717c31058 Update dependency @biomejs/biome to v1.9.1 2024-09-15 18:33:30 +00:00
Athou
693844828b comment tweak 2024-09-15 12:18:56 +02:00
renovate[bot]
ef4b479638 Update quarkus.version to v3.14.4 2024-09-14 16:38:57 +00:00
Athou
8eefb1bcfb add http cache to avoid fetching feeds too often (#1431) 2024-09-14 14:00:15 +02:00
Athou
ada9a5039b add support for all charsets in native mode 2024-09-14 07:56:36 +02:00
Athou
cca2d49cc3 Revert "reduce artifact size by using a smaller library for charset detection" because juniversalchardet doesn't support as many charsets as icu4j 2024-09-13 23:40:13 +02:00
Athou
f4a43e9950 only compute rtl once by storing it in the database on fetch 2024-09-13 22:25:02 +02:00
Athou
9a89b39b62 fix formatting 2024-09-13 21:57:21 +02:00
Jérémie Panzer
2dba844b6c Merge pull request #1552 from victorhck/master
uptade and improve Spanish translation
2024-09-13 21:56:49 +02:00
renovate[bot]
3101dc91de Update dependency vitest to ^2.1.1 2024-09-13 15:51:45 +00:00
Victorhck
83cacd97f3 fix typo Spanish messages.po 2024-09-13 17:19:44 +02:00
Victorhck
aa179c21f8 uptade and improve Spanish translation 2024-09-13 17:17:35 +02:00
Athou
31cf4d8bb2 use our own bidi detector to drop 10Mb from gwt 2024-09-13 16:05:21 +02:00
Athou
19bcc2c0da reduce artifact size by using a smaller library for charset detection 2024-09-13 14:48:24 +02:00
renovate[bot]
ca803ff7ce Update dependency vite to ^5.4.5 2024-09-13 09:42:41 +00:00
Athou
0e26d975aa we don't need quarkus-extension-processor at runtime 2024-09-13 10:55:02 +02:00
Athou
86a3cb67f2 exclude unused database dependencies from final artifact 2024-09-13 10:20:19 +02:00
Jérémie Panzer
6297463445 Merge pull request #1551 from Athou/renovate/com.microsoft.playwright-playwright-1.x
Update dependency com.microsoft.playwright:playwright to v1.47.0
2024-09-13 03:56:06 +02:00
renovate[bot]
1a3a3076b1 Update dependency com.microsoft.playwright:playwright to v1.47.0 2024-09-13 00:06:46 +00:00
Jérémie Panzer
7fb9cfeaf1 Merge pull request #1550 from Athou/renovate/vitest-monorepo
Update dependency vitest to ^2.1.0
2024-09-12 20:46:39 +02:00
renovate[bot]
5c7dbe6304 Update dependency vitest to ^2.1.0 2024-09-12 16:01:52 +00:00
Jérémie Panzer
c41fd9216a Merge pull request #1549 from Athou/renovate/fontsource-monorepo
Update dependency @fontsource/open-sans to ^5.1.0
2024-09-12 16:42:21 +02:00
Jérémie Panzer
91a9ad79f0 Merge pull request #1548 from Athou/renovate/biomejs-biome-1.x
Update dependency @biomejs/biome to v1.9.0
2024-09-12 16:42:10 +02:00
renovate[bot]
906458dc96 Update dependency @fontsource/open-sans to ^5.1.0 2024-09-12 14:21:15 +00:00
renovate[bot]
2f4fddf539 Update dependency @biomejs/biome to v1.9.0 2024-09-12 14:21:09 +00:00
renovate[bot]
a8157143b9 Update dependency io.quarkus.platform:quarkus-bom to v3.14.3 2024-09-11 14:55:31 +00:00
Athou
92576e28e9 fix tests incorrectly always running with h2 2024-09-11 16:49:02 +02:00
Athou
a6e34a2960 no need to repeat the plugin, we can just use the variable 2024-09-11 16:35:08 +02:00
renovate[bot]
306cf7aab7 Update dependency vite to ^5.4.4 2024-09-11 12:55:32 +00:00
Athou
f3b806686d help during development by showing typescript errors for the whole project 2024-09-11 14:54:01 +02:00
Athou
6634cfb991 load from old settings if new settings are not found to smooth transition 2024-09-11 07:52:41 +02:00
Athou
9930bb68b2 rename variable because it now contains a duration 2024-09-10 20:47:08 +02:00
Athou
37722131e5 remove warning about deprecated findDOMNode 2024-09-10 20:16:24 +02:00
Athou
5f2d213419 move other settings to localSettings too 2024-09-10 20:16:24 +02:00
Athou
e119941762 add option to keep some entries above the selected entry when scrolling 2024-09-10 20:16:24 +02:00
Athou
ba496c1395 no longer fetch feeds without subscriptions 2024-09-10 11:16:25 +02:00
renovate[bot]
9c3fc84542 Update dependency react-router-dom to ^6.26.2 2024-09-09 17:31:13 +00:00
Jérémie Panzer
b017ce936a Merge pull request #1546 from Athou/renovate/typescript-5.x
Update dependency typescript to ^5.6.2
2024-09-09 19:30:40 +02:00
renovate[bot]
d696b0581b Update dependency typescript to ^5.6.2 2024-09-09 17:03:23 +00:00
Athou
e4b2880f2b funding links tweaks 2024-09-09 18:56:04 +02:00
renovate[bot]
e071049969 Lock file maintenance 2024-09-09 03:25:09 +00:00
renovate[bot]
5e1f592951 Lock file maintenance 2024-09-09 00:21:56 +00:00
renovate[bot]
231f82da28 Update dependency @fontsource/open-sans to ^5.0.30 2024-09-08 04:53:38 +00:00
Athou
9a28bc7334 release 5.1.1 2024-09-06 11:44:37 +02:00
Athou
00907e92ff indicate that we actually set a boolean value (#1544) 2024-09-06 10:12:05 +02:00
Athou
5669798881 tweak bug report template 2024-09-06 09:09:14 +02:00
Athou
f3a62a5f75 always show current feed/category (#1543) 2024-09-06 09:09:14 +02:00
Jérémie Panzer
3b20ed222c Merge pull request #1542 from Athou/renovate/debian-12.x
Update debian Docker tag to v12.7
2024-09-05 04:42:59 +02:00
renovate[bot]
1f40f3f59c Update debian Docker tag to v12.7 2024-09-05 02:04:26 +00:00
Athou
a8d890524f fix doc 2024-09-04 20:22:24 +02:00
renovate[bot]
b635799e80 Update quarkus.version to v3.14.2 2024-09-04 17:32:46 +00:00
renovate[bot]
50ea66620d Update dependency tss-react to ^4.9.13 2024-09-04 07:10:20 +00:00
Athou
46581d0e22 quarkus has an extension that pulls both junit and mockito 2024-09-04 09:09:28 +02:00
renovate[bot]
a3562867a6 Update dependency vite to ^5.4.3 2024-09-03 19:41:15 +00:00
Athou
c0117ada93 add a link to the feed of the GitHub release page in the README 2024-09-03 19:38:55 +02:00
Jérémie Panzer
a3dcb2c03e Merge pull request #1540 from dai/master
Update messages.po
2024-09-03 10:45:13 +02:00
dai
8f8aaa5a1d Update messages.po
Updated and added Japanese language file.
2024-09-03 16:31:15 +09:00
renovate[bot]
85482422fd Lock file maintenance 2024-09-02 15:50:13 +00:00
Athou
643c969faf release 5.1.0 2024-09-02 17:47:08 +02:00
renovate[bot]
85f9469d6d Update linguijs monorepo to ^4.11.4 2024-09-02 14:07:26 +00:00
renovate[bot]
0df0d50695 Lock file maintenance 2024-09-02 01:33:37 +00:00
Athou
5e9256c197 reduce intermediate database cleanup logging level to reduce logging volume 2024-09-01 21:51:07 +02:00
Athou
b67e1a92f5 fix hibernate warnings about wrong types 2024-09-01 18:21:26 +02:00
Athou
d250e4bc26 add missing foreign key on feedentrystatuses.user_id 2024-09-01 18:02:38 +02:00
Athou
dcf1f41f2d add config doc sections 2024-09-01 13:47:09 +02:00
renovate[bot]
3df6ba1457 Update dependency axios to ^1.7.7 2024-08-31 22:33:06 +00:00
renovate[bot]
b89928f6c6 Update dependency axios to ^1.7.6 2024-08-30 21:06:14 +00:00
renovate[bot]
2e014484e3 Update mantine monorepo to ^7.12.2 2024-08-30 18:47:41 +00:00
renovate[bot]
3b2b18fd2e Update dependency com.puppycrawl.tools:checkstyle to v10.18.1 2024-08-30 15:35:27 +00:00
renovate[bot]
ebf1e592ff Update dependency @types/react to ^18.3.5 2024-08-30 10:40:03 +00:00
renovate[bot]
88404b91d8 Update dependency npm to v10.8.3 2024-08-28 20:30:31 +00:00
Athou
9cbb60313c there are only native implementations of brotli encoders, don't use them because it doesn't work on all platforms 2024-08-28 20:53:48 +02:00
Jérémie Panzer
b95d417f5e Merge pull request #1526 from Athou/renovate/quarkus.version
Update quarkus.version to v3.14.1 (minor)
2024-08-28 19:27:02 +02:00
Athou
994f1fb121 quarkus-config-doc-maven-plugin is now required to generate the documentation 2024-08-28 19:08:34 +02:00
renovate[bot]
e533c1fa4b Update quarkus.version to v3.14.1 2024-08-28 16:31:58 +00:00
renovate[bot]
d0d946ffe9 Update dependency vitest-mock-extended to ^2.0.2 2024-08-28 15:39:32 +00:00
Athou
e3bcc824c7 use a property to sync swagger versions 2024-08-28 17:36:15 +02:00
Athou
357e4e207f add a test to make sure brotli compression is supported 2024-08-28 17:34:24 +02:00
Athou
2aee961600 specify what version of checkstyle we want to use 2024-08-28 09:20:18 +02:00
Athou
3aa1987319 make renovate ignore all "io.quarkus" versions 2024-08-28 09:10:03 +02:00
Jérémie Panzer
ae15f61fc2 Merge pull request #1535 from Athou/renovate/org.apache.maven.plugins-maven-failsafe-plugin-3.x
Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.0
2024-08-27 16:55:50 +02:00
Jérémie Panzer
e58f92a812 Merge pull request #1536 from Athou/renovate/org.apache.maven.plugins-maven-surefire-plugin-3.x
Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.0
2024-08-27 16:55:38 +02:00
renovate[bot]
46383924b1 Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.0 2024-08-27 14:39:46 +00:00
renovate[bot]
071920e864 Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.0 2024-08-27 14:39:42 +00:00
Athou
012238e6a9 Docker README tweak 2024-08-27 13:40:18 +02:00
Athou
a565566c50 remove deprecation warning 2024-08-27 09:02:42 +02:00
Athou
550804c666 use @Transactional where possible 2024-08-27 08:47:39 +02:00
Athou
f7a4a33f5e fix wrong path in documentation 2024-08-27 08:41:53 +02:00
Athou
f1b19ebae3 after reading the spec, what we want is actually "no-cache" that actually means "cache but revalidate immediately" using If-Modified-Since request headers and 304 response codes 2024-08-26 11:41:56 +02:00
Jérémie Panzer
4049fa2e17 Merge pull request #1534 from canoine/master
Update fr/messages.po
2024-08-26 09:43:21 +02:00
canoine
28808cf4f5 Merge pull request #2 from canoine/canoine-patch-1
Update fr/messages.po
2024-08-26 09:07:18 +02:00
canoine
870b46cf9d Update fr/messages.po
Blind update, as I didn't find where some of the new fields are shown.
2024-08-26 08:58:03 +02:00
renovate[bot]
9c20dea99c Lock file maintenance 2024-08-26 02:13:39 +00:00
Athou
63c7679067 make sure the webapp and openapi documentation are always up to date by preventing caching 2024-08-26 00:28:44 +02:00
Athou
764c1a6430 add setting for showing unread count in tab/favicon (#1518) 2024-08-25 20:24:57 +02:00
renovate[bot]
bb6578bdd0 Update dependency org.passay:passay to v1.6.5 2024-08-25 03:23:37 +00:00
renovate[bot]
748c8531ad Lock file maintenance 2024-08-23 19:32:29 +00:00
Athou
a734fe68d2 fix link README 2024-08-23 21:21:15 +02:00
renovate[bot]
cc5ebc55a4 Update dependency axios to ^1.7.5 2024-08-23 14:17:51 +00:00
Jérémie Panzer
aa396c1e1c Merge pull request #1531 from Athou/renovate/monaco-editor-0.x
Update dependency monaco-editor to ^0.51.0
2024-08-23 11:51:22 +02:00
Athou
fbf87ff291 make renovate ignore quarkus-extension-processor 2024-08-23 11:44:24 +02:00
renovate[bot]
e9f3ffddf4 Update dependency monaco-editor to ^0.51.0 2024-08-23 09:33:00 +00:00
Jérémie Panzer
695518d68b Merge pull request #1530 from Athou/renovate/querydsl.version
Update querydsl.version to v6.7 (minor)
2024-08-23 05:34:54 +02:00
renovate[bot]
5d96c1e12b Update querydsl.version to v6.7 2024-08-22 22:39:22 +00:00
Jérémie Panzer
3a72a1cc04 Merge pull request #1529 from Athou/renovate/org.apache.maven.plugins-maven-checkstyle-plugin-3.x
Update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.5.0
2024-08-22 21:15:42 +02:00
renovate[bot]
54f5714108 Update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.5.0 2024-08-22 18:58:15 +00:00
Jérémie Panzer
04811c7eca Merge pull request #1528 from Athou/renovate/org.apache.maven.plugins-maven-help-plugin-3.x
Update dependency org.apache.maven.plugins:maven-help-plugin to v3.5.0
2024-08-22 07:10:46 +02:00
renovate[bot]
6856736ddb Update dependency org.apache.maven.plugins:maven-help-plugin to v3.5.0 2024-08-21 21:04:31 +00:00
Athou
db6c43993a simpler workaround for cookie max-age 2024-08-21 21:56:57 +02:00
Jérémie Panzer
508a22576a Merge pull request #1527 from Athou/renovate/node-20.x
Update dependency node to v20.17.0
2024-08-21 21:35:26 +02:00
renovate[bot]
8fb012b3a1 Update dependency node to v20.17.0 2024-08-21 18:25:57 +00:00
renovate[bot]
133781d314 Update dependency @emotion/react to ^11.13.3 2024-08-21 11:41:31 +00:00
renovate[bot]
50cb12896e Update dependency vite to ^5.4.2 2024-08-21 02:14:50 +00:00
renovate[bot]
79a4315941 Update dependency @types/react to ^18.3.4 2024-08-20 22:13:31 +00:00
renovate[bot]
33a2f76521 Update dependency dayjs to ^1.11.13 2024-08-20 19:25:48 +00:00
Jérémie Panzer
d4041a1d88 Merge pull request #1525 from Athou/renovate/patch-quarkus.version
Update quarkus.version to v3.13.3 (patch)
2024-08-20 21:24:48 +02:00
renovate[bot]
09f2f56446 Update quarkus.version to v3.13.3 2024-08-20 17:16:54 +00:00
Athou
a0c3eda506 remove warning 2024-08-20 10:03:15 +02:00
Athou
84de3199cc guava version is managed by quarkus 2024-08-20 10:01:13 +02:00
Athou
a7e8309d63 fix docker readme missing word 2024-08-20 08:36:31 +02:00
Athou
7e74d2f6f4 release 5.0.2 2024-08-20 07:27:46 +02:00
Athou
dc25d53dc0 github actions is sometimes slow, increase timeout for tests 2024-08-20 00:03:45 +02:00
Athou
ac1a927836 remove google-api-services-youtube because it doesn't play nicely with native-image 2024-08-19 23:55:38 +02:00
Athou
b50b69adb2 Revert "better workaround for cookie max-age" because it causes issues with favicon fetching 2024-08-19 18:27:50 +02:00
Athou
b112e912af release 5.0.1 2024-08-19 15:55:13 +02:00
Athou
ece55727d3 better workaround for cookie max-age 2024-08-19 14:31:12 +02:00
Athou
181dd24b57 format both Dockerfiles the same way 2024-08-19 10:29:35 +02:00
Athou
10008ca0e8 add link to all quarkus settings 2024-08-19 09:38:26 +02:00
Athou
134c4621a8 docker README tweaks 2024-08-19 08:47:23 +02:00
Athou
51f15bf487 github actions is sometimes very slow, increase default timeouts for tests 2024-08-19 08:26:06 +02:00
renovate[bot]
49ae2c88ad Lock file maintenance 2024-08-19 05:53:45 +00:00
Jérémie Panzer
43a628fc55 Merge pull request #1523 from Athou/renovate/org.apache.maven.plugins-maven-surefire-plugin-3.x
Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.4.0
2024-08-19 07:51:49 +02:00
Jérémie Panzer
7f71f95f7c Merge pull request #1522 from Athou/renovate/org.apache.maven.plugins-maven-failsafe-plugin-3.x
Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.4.0
2024-08-19 07:51:41 +02:00
Athou
e8d5eab419 compile to native image with support for older CPUs 2024-08-19 07:08:18 +02:00
renovate[bot]
de3a6b1f20 Update dependency io.dropwizard.metrics:metrics-json to v4.2.27 2024-08-19 00:25:33 +00:00
renovate[bot]
849742e19a Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.4.0 2024-08-18 21:04:13 +00:00
renovate[bot]
b6392b114c Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.4.0 2024-08-18 21:04:10 +00:00
Athou
4db0c775ff add settings documentation 2024-08-18 21:23:42 +02:00
Athou
ff9374f1ed README tweak 2024-08-18 19:20:34 +02:00
Athou
ea86c9bb1f README tweak 2024-08-18 17:07:31 +02:00
Athou
e6dd088abe fix typo 2024-08-18 17:00:59 +02:00
Jérémie Panzer
c039d8f3a4 Merge pull request #1521 from Athou/renovate/com.google.apis-google-api-services-youtube-3.0.x
Update dependency com.google.apis:google-api-services-youtube to v3-rev20240814-2.0.0
2024-08-18 14:51:39 +02:00
renovate[bot]
bffa6329fd Update dependency com.google.apis:google-api-services-youtube to v3-rev20240814-2.0.0 2024-08-18 10:58:38 +00:00
Athou
b88e5d2847 increase ssl handshake timeout during tests to fix build on slower machines 2024-08-18 12:25:52 +02:00
Jérémie Panzer
0fc4fcd406 Merge pull request #1516 from Athou/renovate/react-icons-5.x
Update dependency react-icons to ^5.3.0
2024-08-18 10:39:10 +02:00
Athou
f04ca21394 Twitter has been renamed X 2024-08-18 10:15:17 +02:00
renovate[bot]
a82fca130f Update dependency react-icons to ^5.3.0 2024-08-18 07:58:41 +00:00
Athou
70d494798c Docker readme tweaks 2024-08-18 09:45:44 +02:00
Athou
cf02bf221b release 5.0.0 2024-08-18 09:35:36 +02:00
renovate[bot]
9eb03d7455 Update quarkus.version to v3.13.2 2024-08-18 08:21:57 +02:00
renovate[bot]
12c8fdeec2 Update ibm-semeru-runtimes Docker tag to open-21.0.4_7-jre 2024-08-18 05:58:53 +00:00
Athou
851babfe2a remove quarkus branch from ci 2024-08-18 07:55:51 +02:00
Jérémie Panzer
859490806b Merge pull request #1520 from Athou/quarkus
Migrate to Quarkus (#1517)
2024-08-18 07:54:43 +02:00
renovate[bot]
2c828b50da Update dependency @fontsource/open-sans to ^5.0.29 2024-08-18 01:32:31 +00:00
Athou
ede7834cb8 configurable filtering expression evaluation timeout 2024-08-17 23:26:42 +02:00
Athou
3627ee369d version dockerhub readme and update it automatically on release 2024-08-17 23:16:19 +02:00
Athou
c4c41d1494 increase timeout a little bit because github actions are laggy 2024-08-17 22:29:40 +02:00
Athou
c577e77f8f README tweaks 2024-08-17 22:27:30 +02:00
Athou
9218f19832 javadoc tweaks 2024-08-17 22:11:47 +02:00
renovate[bot]
ecbc2133a4 Update dependency maven to v3.9.9 2024-08-17 20:10:04 +00:00
Athou
e38ca66c51 try to fix "Illegal attempt to associate a ManagedEntity with two open persistence contexts" 2024-08-17 16:22:46 +02:00
Athou
2395a2670e add ResourceBundle needed for cssparser in native image 2024-08-17 00:43:32 +02:00
Athou
e7748d787f no need to start a transaction to fetch favicons 2024-08-16 22:29:12 +02:00
Athou
012ce71134 configurable http client timeouts 2024-08-16 21:16:02 +02:00
Athou
1b1a3f49c1 keep using the same css parser as before 2024-08-16 19:01:44 +02:00
Athou
5b77860189 use join to speed up cleanup 2024-08-16 17:42:15 +02:00
Athou
b333e8d90a set a timeout on ssl handshakes 2024-08-16 14:40:16 +02:00
Athou
ab6457ef3f README tweaks 2024-08-16 14:23:18 +02:00
Athou
5c69daec08 restore welcome page on 401 2024-08-16 14:02:49 +02:00
Athou
1bfa3ebb8e set the default h2 path to a relative one next to the executable 2024-08-16 13:47:15 +02:00
Athou
2694fea211 cleanup 2024-08-16 08:45:30 +02:00
Athou
720eddeb66 CHANGELOG tweaks 2024-08-16 08:31:23 +02:00
Athou
ab334a7bc6 feed needs to be known to be deleted 2024-08-16 07:31:22 +02:00
Athou
214dfe580a more rome classes 2024-08-16 07:22:25 +02:00
Athou
4ef53eab3a mute rome modules warnings in production 2024-08-16 06:19:35 +02:00
Athou
2f51547f0d add missing rome classes 2024-08-16 06:15:52 +02:00
Athou
da910ac336 mute MediaModuleParser warnings 2024-08-16 05:53:23 +02:00
Athou
643954f7c9 timeout should be an Integer 2024-08-16 05:48:01 +02:00
renovate[bot]
63061482d0 Update dependency vite to ^5.4.1 2024-08-15 19:16:05 +00:00
renovate[bot]
86d4f5a670 Update dependency react-router-dom to ^6.26.1 2024-08-15 17:31:29 +00:00
Athou
815093f1c6 remove STARTUP_TIME because static fields are initialized at compile time in native mode 2024-08-15 12:17:28 +02:00
Athou
47d39831d3 use Duration for query timeout 2024-08-15 09:05:56 +02:00
Athou
c18ed829aa generate jvm package with a -jvm suffix 2024-08-14 23:08:24 +02:00
Athou
e757e61b79 config comment tweaks 2024-08-14 21:34:05 +02:00
Athou
d612d83874 resolve public url dynamically, remove publicUrl config element 2024-08-14 21:07:45 +02:00
Athou
e170dfe60b prepare 5.0.0 changelog 2024-08-14 20:43:54 +02:00
Athou
69cd90edd8 only use rest-assured for tests 2024-08-14 16:00:47 +02:00
renovate[bot]
f506f722c2 Update dependency axios to ^1.7.4 2024-08-13 20:16:28 +00:00
Athou
857736adad README tweaks 2024-08-13 17:26:51 +02:00
Athou
a92df774bd rome needs to clone Date in native mode 2024-08-13 16:13:05 +02:00
Athou
f2c6734c79 fix warning in native mode about parser not found 2024-08-13 16:12:40 +02:00
Athou
77b6cf75a5 README tweaks 2024-08-13 15:05:40 +02:00
Athou
3b56496196 javadoc tweaks 2024-08-13 12:49:04 +02:00
Athou
aabbf0a5d1 use a relative link 2024-08-13 12:49:04 +02:00
Athou
9a43fd434f ci for quarkus branch 2024-08-13 12:49:04 +02:00
Athou
21ce9db4b0 README update 2024-08-13 12:49:04 +02:00
Athou
044694487d remove redis as caching is no longer needed now 2024-08-13 12:49:04 +02:00
Athou
3af8485326 TODO remove redis 2024-08-13 12:49:04 +02:00
Athou
f7adef0648 add windows builds 2024-08-13 12:49:04 +02:00
Athou
dc16e43154 add release ci job 2024-08-13 12:49:04 +02:00
Athou
78a5267198 fix opml import encoding issue 2024-08-13 12:49:04 +02:00
Athou
04af355e0c remove unused timers page 2024-08-13 12:49:04 +02:00
Athou
89405009ec set a Max-Age on the auth cookie 2024-08-13 12:49:04 +02:00
Athou
6b0aa32da2 use profile instead of system property to set db-kind 2024-08-13 12:49:04 +02:00
Athou
aaf237d111 use quarkus mailer for password recovery 2024-08-13 12:49:04 +02:00
Athou
1fd48a0a40 merge docker amd64 and arm64 tags 2024-08-13 12:49:03 +02:00
Athou
09e0a51b46 restore Docker workflow 2024-08-13 12:49:00 +02:00
Athou
cc32f8ad16 WIP 2024-08-13 12:48:37 +02:00
renovate[bot]
2f6ddf0e70 Update mariadb Docker tag to v11.4.3 2024-08-13 00:48:48 +00:00
renovate[bot]
c3973755da Update ibm-semeru-runtimes Docker tag to open-21.0.4_7-jre 2024-08-12 23:34:26 +00:00
renovate[bot]
42537a65b9 Update dependency com.h2database:h2 to v2.3.232 2024-08-12 19:41:12 +00:00
Athou
906c92e54f fix badge layout 2024-08-12 21:13:46 +02:00
renovate[bot]
cc69968d78 Update mantine monorepo to ^7.12.1 2024-08-12 14:48:18 +00:00
renovate[bot]
dcde2083ec Lock file maintenance 2024-08-12 01:26:56 +00:00
Jérémie Panzer
7469784059 Merge pull request #1514 from Athou/renovate/com.microsoft.playwright-playwright-1.x
Update dependency com.microsoft.playwright:playwright to v1.46.0
2024-08-10 12:14:13 +02:00
renovate[bot]
c13a693456 Update dependency com.microsoft.playwright:playwright to v1.46.0 2024-08-09 23:05:37 +00:00
Jérémie Panzer
e3c482d664 Merge pull request #1510 from Athou/renovate/vite-5.x
Update dependency vite to ^5.4.0
2024-08-09 16:48:43 +02:00
renovate[bot]
1fd33a5585 Update dependency vite to ^5.4.0 2024-08-09 14:34:49 +00:00
Jérémie Panzer
0742778e6a Merge pull request #1513 from Athou/renovate/patch-linguijs-monorepo
Update linguijs monorepo to ^4.11.3 (patch)
2024-08-09 16:33:03 +02:00
Jérémie Panzer
152479c888 Merge pull request #1511 from Athou/renovate/vite-tsconfig-paths-5.x
Update dependency vite-tsconfig-paths to v5
2024-08-09 16:32:23 +02:00
Jérémie Panzer
a297f8c0c8 Merge pull request #1512 from Athou/renovate/postgres-16.x
Update postgres Docker tag to v16.4
2024-08-09 16:31:59 +02:00
renovate[bot]
92aeee0572 Update linguijs monorepo to ^4.11.3 2024-08-09 11:41:52 +00:00
renovate[bot]
050756517e Update dependency vite-tsconfig-paths to v5 2024-08-08 22:34:31 +00:00
renovate[bot]
0bb46f291a Update postgres Docker tag to v16.4 2024-08-08 22:34:10 +00:00
Jérémie Panzer
1eecabf105 Merge pull request #1508 from Athou/renovate/mantine-monorepo
Update mantine monorepo to ^7.12.0 (minor)
2024-08-05 19:16:47 +02:00
renovate[bot]
da1bd8d32e Update mantine monorepo to ^7.12.0 2024-08-05 16:28:35 +00:00
Athou
124983a396 rename metrics a little 2024-08-05 09:05:01 +02:00
Athou
43613688da evict unused HTTP connections 2024-08-05 08:41:12 +02:00
Athou
43aa69cd18 don't crash vite on error in dev mode 2024-08-05 08:28:17 +02:00
Athou
780b7666c5 add metrics for HttpGetter connection pool 2024-08-05 08:28:17 +02:00
renovate[bot]
70b4534e14 Lock file maintenance 2024-08-05 01:13:51 +00:00
Athou
24666fd7fc migrate to lingui.config.ts to benefit from typing 2024-08-03 22:29:09 +02:00
Athou
de80aa6bb3 replace t` with msg` to fix labels not being translated correctly 2024-08-03 13:09:15 +02:00
Athou
6c7e2ea847 add missing translation key for the Cmd key on MacOS 2024-08-03 12:56:56 +02:00
Athou
6ea318acd3 remove right section as we don't show keyboard shortcuts anywhere else 2024-08-03 12:54:57 +02:00
Athou
2f4ee7cff8 add data attributes to tree elements (#1507) 2024-08-03 12:37:33 +02:00
Athou
9d9d758fa6 use article instead of div (#1507) 2024-08-03 11:34:36 +02:00
Athou
a071b7c265 add aria-label to action buttons (#1507) 2024-08-03 11:30:29 +02:00
Athou
3a57b68fa3 use a different icon for filtering unread entries and marking an entry as read (#1506) 2024-08-03 10:24:06 +02:00
Jérémie Panzer
f2f36baf1b Merge pull request #1505 from Athou/renovate/querydsl.version
Update querydsl.version to v6.6 (minor)
2024-08-02 19:28:04 +02:00
renovate[bot]
1aaf9e747a Update querydsl.version to v6.6 2024-08-02 17:00:53 +00:00
Athou
92611772a9 reduce bottom margin slightly to avoid having a scrollbar in the extension popup when there are no entries 2024-08-02 10:37:40 +02:00
renovate[bot]
fb159dc46b Update dependency axios to ^1.7.3 2024-08-01 16:19:28 +00:00
Jérémie Panzer
78ea0873f2 Merge pull request #1504 from Athou/renovate/react-router-monorepo
Update dependency react-router-dom to ^6.26.0
2024-08-01 18:18:45 +02:00
renovate[bot]
faabc01dbc Update dependency react-router-dom to ^6.26.0 2024-08-01 13:48:59 +00:00
renovate[bot]
5acfe9e92a Update dependency tss-react to ^4.9.12 2024-08-01 12:45:12 +00:00
renovate[bot]
4388a8b6ce Update testcontainers-java monorepo to v1.20.1 2024-07-31 16:31:47 +00:00
renovate[bot]
7414bd15b0 Update dependency vitest to ^2.0.5 2024-07-31 13:41:55 +00:00
Jérémie Panzer
52d6021f3c Merge pull request #1503 from Athou/renovate/redis-7.x
Update redis Docker tag to v7.4.0
2024-07-30 07:14:48 +02:00
renovate[bot]
f7acc27fcb Update redis Docker tag to v7.4.0 2024-07-30 04:46:37 +00:00
renovate[bot]
175a293327 Update dependency redis.clients:jedis to v5.1.4 2024-07-29 17:17:09 +00:00
renovate[bot]
21dd6519b0 Update bouncycastle.version to v1.78.1 2024-07-29 11:25:29 +00:00
Athou
72f55c34b7 add test to make sure HttpGetter supports compression 2024-07-29 13:20:18 +02:00
Athou
1b1d3c791b add test to make sure HttpGetter ignores invalid certificates 2024-07-29 10:39:35 +02:00
renovate[bot]
159c2c01a7 Lock file maintenance 2024-07-29 00:18:18 +00:00
Athou
272f5b42f9 simplify stackoverflow urls 2024-07-28 09:58:24 +02:00
renovate[bot]
2395d0782e Update dependency @reduxjs/toolkit to ^2.2.7 2024-07-27 18:19:48 +00:00
Athou
da81830e43 add a test case for feeds that do not return a content length 2024-07-27 01:01:11 +02:00
renovate[bot]
63a602cf8a Update dependency tss-react to ^4.9.11 2024-07-26 16:23:31 +00:00
renovate[bot]
0244b5c3e3 Update dependency vite to ^5.3.5 2024-07-25 12:40:59 +00:00
Jérémie Panzer
9592e86fa9 Merge pull request #1497 from Athou/renovate/node-20.x
Update dependency node to v20.16.0
2024-07-24 21:11:44 +02:00
renovate[bot]
e6840bb50c Update dependency node to v20.16.0 2024-07-24 16:31:02 +00:00
Jérémie Panzer
b6890378a1 Merge pull request #1496 from Athou/renovate/vitest-mock-extended-2.x
Update dependency vitest-mock-extended to v2
2024-07-23 21:56:08 +02:00
renovate[bot]
ba72ed0b93 Update dependency vitest-mock-extended to v2 2024-07-23 19:45:08 +00:00
renovate[bot]
e2fb576858 Update ibm-semeru-runtimes Docker tag to open-21.0.3_9-jre 2024-07-23 15:40:19 +00:00
Athou
608b099b4d make renovate pickup semeru 2024-07-23 17:39:37 +02:00
renovate[bot]
c2e0c81f7e Update mysql Docker tag to v9.0.1 2024-07-23 07:16:23 +00:00
renovate[bot]
7071d01a59 Update dependency typescript to ^5.5.4 2024-07-23 04:26:11 +00:00
renovate[bot]
30cd2b9b53 Update dependency com.microsoft.playwright:playwright to v1.45.1 2024-07-23 00:05:41 +00:00
Athou
abc498b09c semeru already defines a JAVA_TOOL_OPTIONS variable with a shared classes cache, we don't want to override it 2024-07-22 16:52:30 +02:00
renovate[bot]
31081e1089 Update dependency vitest to ^2.0.4 2024-07-22 09:53:53 +00:00
renovate[bot]
4a16b8d072 Lock file maintenance 2024-07-22 02:12:54 +00:00
Jérémie Panzer
9c04095292 Merge pull request #1492 from Athou/renovate/emotion-monorepo
Update dependency @emotion/react to ^11.13.0
2024-07-20 14:55:31 +02:00
renovate[bot]
643f98d59e Update dependency @emotion/react to ^11.13.0 2024-07-20 09:06:33 +00:00
Jérémie Panzer
f4da19183e Merge pull request #1491 from Athou/renovate/emotion-monorepo
Update dependency @emotion/react to ^11.12.0
2024-07-19 09:56:36 +02:00
renovate[bot]
de40f253b5 Update dependency @emotion/react to ^11.12.0 2024-07-19 07:28:55 +00:00
renovate[bot]
f6543e407a Update dependency dayjs to ^1.11.12 2024-07-18 13:26:56 +00:00
renovate[bot]
4d462a8e9e Update dependency react-router-dom to ^6.25.1 2024-07-17 22:23:50 +00:00
Jérémie Panzer
018ee1f3e6 Merge pull request #1490 from Athou/renovate/testcontainers-java-monorepo
Update testcontainers-java monorepo to v1.20.0 (minor)
2024-07-18 00:22:46 +02:00
renovate[bot]
752268fed1 Update testcontainers-java monorepo to v1.20.0 2024-07-17 22:10:29 +00:00
renovate[bot]
8fe9a6cc3a Update dependency org.mariadb.jdbc:mariadb-java-client to v3.4.1 2024-07-17 20:48:37 +00:00
Athou
b17a17ba10 don't parse feeds that are too large to prevent memory issues 2024-07-16 21:20:06 +02:00
renovate[bot]
b3545b60ea Update dependency vite to ^5.3.4 2024-07-16 13:43:12 +00:00
Jérémie Panzer
e6da3f693d Merge pull request #1488 from Athou/renovate/react-router-monorepo
Update dependency react-router-dom to ^6.25.0
2024-07-16 15:42:04 +02:00
renovate[bot]
4ab09da434 Update dependency react-router-dom to ^6.25.0 2024-07-16 13:28:04 +00:00
renovate[bot]
5e8daf29bf Update dependency vitest-mock-extended to ^1.3.2 2024-07-15 21:14:52 +00:00
Athou
024a1067bb update h2 2024-07-15 21:48:49 +02:00
renovate[bot]
c427da72b9 Update dependency vitest to ^2.0.3 2024-07-15 14:05:06 +00:00
Athou
346fb6b1ea release 4.6.0 2024-07-15 16:03:09 +02:00
Athou
1b658c76a3 show both read and unread entries when searching with keywords 2024-07-15 12:41:13 +02:00
Athou
1593ed62ba github actions is slow, increase timeout 2024-07-15 11:11:19 +02:00
Athou
085eddd4b0 fill the shared classes cache of openj9 even more 2024-07-15 10:57:26 +02:00
Jérémie Panzer
0db77ad2c0 Merge pull request #1487 from Athou/renovate/com.manticore-projects.tools-h2migrationtool-1.x
Update dependency com.manticore-projects.tools:h2migrationtool to v1.7
2024-07-15 10:41:13 +02:00
renovate[bot]
6f8bcb6c6a Update dependency com.manticore-projects.tools:h2migrationtool to v1.7 2024-07-15 07:43:51 +00:00
renovate[bot]
4196dee896 Lock file maintenance 2024-07-15 01:09:26 +00:00
Athou
6d49e0f0df build openj9 shared classes cache to improve startup time 2024-07-14 22:26:39 +02:00
Athou
d99f572989 move env variable definition before adding files in order to maximize layer reusability 2024-07-14 21:14:39 +02:00
Athou
fa197c33f1 rename field accordingly 2024-07-14 20:37:01 +02:00
Athou
1ce39a419e use "published" instead of "updated" (#1486) 2024-07-14 19:53:35 +02:00
Athou
f0e3ac8fcb README tweaks 2024-07-14 09:35:44 +02:00
renovate[bot]
30947cea05 Update mantine monorepo to ^7.11.2 2024-07-13 15:46:24 +00:00
Athou
9134f36d3b use openj9 as the Java runtime to reduce memory usage 2024-07-13 13:48:34 +02:00
Athou
dc526316a0 enable string deduplication to reduce memory usage 2024-07-13 10:25:32 +02:00
renovate[bot]
6593174668 Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.3.1 2024-07-11 01:02:47 +00:00
renovate[bot]
0891c41abc Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.3.1 2024-07-10 22:28:34 +00:00
renovate[bot]
6ecb6254aa Update dependency npm to v10.8.2 2024-07-10 19:02:33 +00:00
renovate[bot]
84bd9eeeff Update dependency vitest to ^2.0.2 2024-07-10 16:49:16 +00:00
Jérémie Panzer
2549c4d47b Merge pull request #1485 from Athou/renovate/org.jsoup-jsoup-1.x
Update dependency org.jsoup:jsoup to v1.18.1
2024-07-10 13:16:58 +02:00
renovate[bot]
8750aa3dd6 Update dependency org.jsoup:jsoup to v1.18.1 2024-07-10 11:02:42 +00:00
Athou
262094a736 remove dangling comment 2024-07-10 08:55:45 +02:00
Jérémie Panzer
035201f917 Merge pull request #1483 from Athou/renovate/major-vitest-monorepo
Update dependency vitest to v2
2024-07-09 03:47:48 +02:00
Jérémie Panzer
ae9cbc5214 Merge pull request #1484 from Athou/renovate/node-20.15.x
Update dependency node to v20.15.1
2024-07-09 03:42:53 +02:00
Athou
78d5bf129a fix build 2024-07-09 03:42:38 +02:00
renovate[bot]
1f02ddd163 Update dependency node to v20.15.1 2024-07-08 20:17:38 +00:00
renovate[bot]
eff1e8cc7b Update dependency vitest to v2 2024-07-08 16:18:13 +00:00
Jérémie Panzer
dc8475b59a Merge pull request #1482 from Athou/renovate/lock-file-maintenance
Lock file maintenance
2024-07-08 07:05:27 +02:00
renovate[bot]
921968662d Lock file maintenance 2024-07-08 02:21:45 +00:00
Athou
4d83173dbd release 4.5.0 2024-07-06 21:29:21 +02:00
Athou
f13368cb96 remove unnecessary joins 2024-07-06 13:40:53 +02:00
Athou
ec7e97e1de abort current request if we're changing what we're going to display 2024-07-04 07:19:16 +02:00
Athou
d4c9bd1dd7 remove warnings 2024-07-03 20:11:51 +02:00
renovate[bot]
6bff657d4d Update linguijs monorepo to ^4.11.2 2024-07-03 17:13:52 +00:00
renovate[bot]
613d286be1 Update dependency react-router-dom to ^6.24.1 2024-07-03 16:05:19 +00:00
Athou
fd48108f8b don't rely on dates to know if an entry has been inserted in the database 2024-07-03 17:53:41 +02:00
Athou
c3cbd18df9 add debug logging 2024-07-03 17:40:23 +02:00
Athou
6685057dae notify over websocket after everything has been committed 2024-07-03 17:27:17 +02:00
Athou
0dec0e3788 fix a race condition where a feed could be refreshed before it was created 2024-07-03 14:21:40 +02:00
Athou
1a73dd4004 the feed refresh engine is now fast enough, it doesn't need workarounds anymore 2024-07-03 13:30:25 +02:00
Athou
eae80a6450 remove support for microsoft sqlserver, AFAIK nobody's using it and it's not covered with integration tests 2024-07-03 13:02:20 +02:00
Jérémie Panzer
21a32ce0eb Merge pull request #1479 from Athou/renovate/mysql-9.x
Update mysql Docker tag to v9
2024-07-03 11:35:35 +02:00
renovate[bot]
325533c5d9 Update mysql Docker tag to v9 2024-07-03 09:10:22 +00:00
renovate[bot]
7d819022f6 Update redis Docker tag to v7.2.5 2024-07-03 09:10:19 +00:00
Athou
dba944874b add redis integration test 2024-07-03 11:09:23 +02:00
Jérémie Panzer
ce9c12ec92 Merge pull request #1478 from Athou/renovate/postgres-16.x
Update postgres Docker tag to v16.3
2024-07-03 09:23:35 +02:00
Jérémie Panzer
22dfc5774f Merge pull request #1477 from Athou/renovate/mariadb-11.x
Update mariadb Docker tag to v11.4.2
2024-07-03 09:23:24 +02:00
renovate[bot]
d59091ab2b Update postgres Docker tag to v16.3 2024-07-03 07:08:14 +00:00
renovate[bot]
f69146a6bf Update mariadb Docker tag to v11.4.2 2024-07-03 07:08:11 +00:00
Athou
43cdf3db3b add integration tests for postgresql, mysql and mariadb using testcontainers 2024-07-03 09:02:18 +02:00
renovate[bot]
280a354228 Update dependency vite-plugin-biome to ^1.0.12 2024-07-03 06:39:02 +00:00
renovate[bot]
573b0431f9 Update dependency vite to ^5.3.3 2024-07-03 06:32:04 +00:00
renovate[bot]
9878b60e97 Update dependency io.github.git-commit-id:git-commit-id-maven-plugin to v9.0.1 2024-07-02 18:02:54 +00:00
renovate[bot]
964033c2a7 Update mantine monorepo to ^7.11.1 2024-07-02 14:12:43 +00:00
Jérémie Panzer
d2e45aca91 Merge pull request #1475 from Athou/renovate/com.mysql-mysql-connector-j-9.x
Update dependency com.mysql:mysql-connector-j to v9
2024-07-02 16:11:43 +02:00
renovate[bot]
daa99a2efc Update dependency com.mysql:mysql-connector-j to v9 2024-07-02 10:08:39 +00:00
Jérémie Panzer
e986e9999a Merge pull request #1473 from Athou/renovate/node-20.x
Update dependency node to v20.15.0
2024-07-02 09:19:55 +02:00
Jérémie Panzer
98d302cb94 Merge pull request #1474 from Athou/renovate/npm-10.x
Update dependency npm to v10.8.1
2024-07-02 09:19:44 +02:00
renovate[bot]
bf11c4a7e4 Update dependency npm to v10.8.1 2024-07-02 07:13:46 +00:00
renovate[bot]
e1cab952f8 Update dependency node to v20.15.0 2024-07-02 07:13:42 +00:00
Athou
bc28d4de27 renovate can now update node and npm 2024-07-02 09:12:42 +02:00
renovate[bot]
bb901564e3 Update dependency typescript to ^5.5.3 2024-07-01 22:21:16 +00:00
Athou
93acc9ded1 we don't need the user we already have the subscription 2024-07-01 13:55:50 +02:00
renovate[bot]
9b1c6a371e Lock file maintenance 2024-07-01 07:50:40 +00:00
Athou
82bf8cd807 fetch all tags at once 2024-07-01 08:52:05 +02:00
Athou
c2f2780c3f remove unused onlyIds parameter 2024-07-01 08:52:05 +02:00
Athou
08f71d1f6f implement faster querying by fetching directly what we need 2024-07-01 08:52:05 +02:00
Athou
f498088beb no need to insert statuses that will be collected during next cleanup 2024-06-30 21:56:42 +02:00
Athou
347b41cf35 fix exception when trying to mark starred entries as read 2024-06-30 16:50:37 +02:00
renovate[bot]
61ae90ad28 Update dependency @reduxjs/toolkit to ^2.2.6 2024-06-29 14:12:13 +00:00
Athou
9a42fbafb2 only automerge patch updates, keep creating pull requests for minor updates 2024-06-29 08:47:49 +02:00
Athou
938f9e9434 remove automergePr because we now specify automergeBranch 2024-06-28 07:27:58 +02:00
Jérémie Panzer
9004e453c2 Merge pull request #1470 from Athou/renovate/querydsl.version
Update querydsl.version to v6.5
2024-06-28 07:26:21 +02:00
renovate[bot]
7d33542691 Update querydsl.version to v6.5 2024-06-28 05:25:56 +00:00
Athou
c99348862c reduce renovate noise by automerging if tests pass 2024-06-28 07:25:10 +02:00
Jérémie Panzer
ac86db3966 Merge pull request #1463 from Athou/renovate/com.manticore-projects.tools-h2migrationtool-1.x
Update dependency com.manticore-projects.tools:h2migrationtool to v1.6
2024-06-28 07:21:29 +02:00
Athou
e368810731 adapt script to new H2MigrationTool file output name 2024-06-28 07:16:42 +02:00
renovate[bot]
edae2f5a61 Update dependency com.manticore-projects.tools:h2migrationtool to v1.6 2024-06-28 03:34:34 +00:00
renovate[bot]
ab17c6f44e Update dependency org.projectlombok:lombok to v1.18.34 2024-06-28 03:34:21 +00:00
renovate[bot]
59dbae4f66 Update dependency com.microsoft.playwright:playwright to v1.45.0 2024-06-28 01:23:51 +00:00
renovate[bot]
d7956292df Update dependency vite-plugin-biome to ^1.0.11 2024-06-27 21:19:36 +00:00
renovate[bot]
1075497559 Update dependency vite to ^5.3.2 2024-06-27 19:44:59 +00:00
renovate[bot]
2d99fa03d3 Update dependency @biomejs/biome to v1.8.3 2024-06-27 16:49:21 +00:00
Jérémie Panzer
72b64b6f0d Merge pull request #1465 from Athou/renovate/mantine-monorepo
Update mantine monorepo to ^7.11.0
2024-06-27 09:35:00 +02:00
Jérémie Panzer
a2096d3622 Merge pull request #1457 from Athou/renovate/monaco-editor-0.x
Update dependency monaco-editor to ^0.50.0
2024-06-27 09:34:52 +02:00
Jérémie Panzer
c81f9fb7b1 Merge pull request #1459 from Athou/renovate/throttle-debounce-5.x
Update dependency throttle-debounce to ^5.0.2
2024-06-27 09:34:44 +02:00
Jérémie Panzer
cc7e9e21fb Merge pull request #1461 from Athou/renovate/react-router-monorepo
Update dependency react-router-dom to ^6.24.0
2024-06-27 09:34:35 +02:00
Jérémie Panzer
803d537e51 Merge pull request #1456 from Athou/renovate/biomejs-biome-1.x
Update dependency @biomejs/biome to v1.8.2
2024-06-27 09:34:20 +02:00
Jérémie Panzer
9a83e5b6ef Merge pull request #1458 from Athou/renovate/typescript-5.x
Update dependency typescript to ^5.5.2
2024-06-27 09:34:08 +02:00
renovate[bot]
4323da9007 Update mantine monorepo to ^7.11.0 2024-06-27 07:28:56 +00:00
renovate[bot]
30b9b24be4 Update dependency typescript to ^5.5.2 2024-06-27 07:28:42 +00:00
renovate[bot]
b191b00003 Update dependency react-router-dom to ^6.24.0 2024-06-27 07:28:30 +00:00
renovate[bot]
7e5cdcba34 Update dependency monaco-editor to ^0.50.0 2024-06-27 07:28:16 +00:00
renovate[bot]
45b30ad333 Update dependency throttle-debounce to ^5.0.2 2024-06-27 07:27:58 +00:00
renovate[bot]
7ca087b0a6 Update dependency @biomejs/biome to v1.8.2 2024-06-27 07:27:47 +00:00
Jérémie Panzer
188e4594fd automerge small dependency updates if they pass tests 2024-06-27 09:26:48 +02:00
Jérémie Panzer
2da80ce7d8 Merge pull request #1453 from Athou/renovate/org.apache.maven.plugins-maven-jar-plugin-3.x
Update dependency org.apache.maven.plugins:maven-jar-plugin to v3.4.2
2024-06-20 14:09:39 +02:00
renovate[bot]
d5820f9aa5 Update dependency org.apache.maven.plugins:maven-jar-plugin to v3.4.2 2024-06-19 18:42:15 +00:00
Athou
b1a0aae0a5 treat javac warnings as errors 2024-06-19 19:41:09 +02:00
Athou
cdd4d4b063 change BaseIT test class so that authentication with the "admin" user is not the default 2024-06-19 19:41:02 +02:00
Jérémie Panzer
21f675e80b Merge pull request #1451 from Athou/renovate/querydsl.version
Update querydsl.version to v6.4
2024-06-19 18:53:35 +02:00
renovate[bot]
380724d73e Update querydsl.version to v6.4 2024-06-19 16:49:05 +00:00
Athou
2d26c5dee3 remove empty file 2024-06-18 20:40:36 +02:00
Jérémie Panzer
29bcc5ccf5 Merge pull request #1437 from Athou/renovate/maven-3.x
Update dependency maven to v3.9.8
2024-06-17 22:31:07 +02:00
Jérémie Panzer
91497ab45a Merge pull request #1450 from Athou/renovate/docker-build-push-action-6.x
Update docker/build-push-action action to v6
2024-06-17 22:30:49 +02:00
renovate[bot]
be77968570 Update docker/build-push-action action to v6 2024-06-17 10:38:49 +00:00
renovate[bot]
a42dacc48d Update dependency maven to v3.9.8 2024-06-17 10:38:45 +00:00
Athou
cd06055246 release 4.4.1 2024-06-15 14:52:26 +02:00
Jérémie Panzer
62c1f25ffc Merge pull request #1448 from Athou/renovate/vite-5.x
Update dependency vite to ^5.3.1
2024-06-15 14:42:42 +02:00
Jérémie Panzer
415dc15d6c Merge pull request #1449 from Athou/renovate/mantine-monorepo
Update mantine monorepo to ^7.10.2
2024-06-15 14:41:52 +02:00
renovate[bot]
1d0c87c679 Update mantine monorepo to ^7.10.2 2024-06-15 12:32:58 +00:00
renovate[bot]
e51c486a04 Update dependency vite to ^5.3.1 2024-06-15 12:32:47 +00:00
Athou
73808c1a70 make renovate also bump versions in package.json 2024-06-15 14:32:05 +02:00
Athou
fbcc2ecd0f add a little delay to simulate a network operation 2024-06-15 14:00:15 +02:00
Athou
3997606774 looks like sometimes the websocket connection is not established on github actions, refresh the tree with an interval smaller than the timeout of playwright 2024-06-15 08:46:55 +02:00
Jérémie Panzer
b988b599d5 Merge pull request #1446 from Athou/renovate/org.apache.maven.plugins-maven-failsafe-plugin-3.x
Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.3.0
2024-06-14 22:31:42 +02:00
Jérémie Panzer
3e2ff2959d Merge pull request #1447 from Athou/renovate/org.apache.maven.plugins-maven-surefire-plugin-3.x
Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.3.0
2024-06-14 22:31:31 +02:00
renovate[bot]
5714a63d27 Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.3.0 2024-06-14 19:55:02 +00:00
renovate[bot]
12b18d1e04 Update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.3.0 2024-06-14 19:54:59 +00:00
Athou
232141cb56 fix UnsupportedTemporalTypeException when tests fail 2024-06-14 21:50:50 +02:00
Jérémie Panzer
c4334e5e6e Merge pull request #1445 from Athou/renovate/vite-5.x-lockfile
Update dependency vite to v5.3.1
2024-06-14 13:00:43 +02:00
renovate[bot]
ddf78f880b Update dependency vite to v5.3.1 2024-06-14 09:59:39 +00:00
Athou
b3651f3fba upload playwright artifacts on test failure to help debug what went wrong 2024-06-14 07:59:26 +02:00
Jérémie Panzer
24943b868c Merge pull request #1442 from Athou/renovate/mantine-monorepo
Update mantine monorepo to v7.10.2
2024-06-13 23:51:58 +02:00
renovate[bot]
ef71a691ef Update mantine monorepo to v7.10.2 2024-06-13 21:47:30 +00:00
Jérémie Panzer
01593d94eb Merge pull request #1443 from Athou/renovate/vite-5.x-lockfile
Update dependency vite to v5.3.0
2024-06-13 23:45:28 +02:00
renovate[bot]
b793cc66d1 Update dependency vite to v5.3.0 2024-06-13 21:31:42 +00:00
Athou
3810dedf47 replace complex eslint config with biome 2024-06-13 23:28:45 +02:00
Athou
9115797dee try to fix renovatebot warning about not being able to update commafeed-client 2024-06-13 06:53:23 +02:00
Athou
232658b934 remove commons-io since we already have guava 2024-06-12 17:17:54 +02:00
Athou
f99fe57695 remove 32bit arm7 because its support was dropped from temurin 21 2024-06-12 16:40:08 +02:00
Athou
ec89d41112 update mvn wrapper 2024-06-12 16:36:57 +02:00
Jérémie Panzer
f6d26a77cc Merge pull request #1439 from Athou/renovate/eclipse-temurin-21.x
Update eclipse-temurin Docker tag to v21
2024-06-12 16:33:07 +02:00
renovate[bot]
860852cc12 Update eclipse-temurin Docker tag to v21 2024-06-12 14:28:49 +00:00
Athou
d06d76401c download maven-wrapper binaries if needed 2024-06-12 16:24:26 +02:00
Athou
f5b04a783e remove commons-codec since we already have guava 2024-06-12 16:18:52 +02:00
Athou
964e470951 manually bump dependencies left behind by dependabot 2024-06-12 15:45:58 +02:00
Jérémie Panzer
612f8722dd Merge pull request #1433 from Athou/renovate/prettier-3.x-lockfile
Update dependency prettier to v3.3.2
2024-06-12 15:33:22 +02:00
Jérémie Panzer
e118dc9b7f Merge pull request #1434 from Athou/renovate/eclipse-temurin-17.x
Update eclipse-temurin Docker tag to v17.0.11_9-jre
2024-06-12 15:32:49 +02:00
renovate[bot]
6e42cdaf2d Update eclipse-temurin Docker tag to v17.0.11_9-jre 2024-06-12 13:28:00 +00:00
renovate[bot]
5198792ca5 Update dependency prettier to v3.3.2 2024-06-12 13:27:56 +00:00
Athou
10a71213f3 use renovate instead of dependabot 2024-06-12 15:27:48 +02:00
Jérémie Panzer
a5d0979d9f Merge pull request #1432 from Athou/renovate/configure
Configure Renovate
2024-06-12 15:27:13 +02:00
renovate[bot]
d84225ab1c Add renovate.json 2024-06-12 13:23:16 +00:00
Athou
cd86947e64 keep pull to refresh for safari (#1168) 2024-06-12 13:12:33 +02:00
Athou
f6b3114a91 use react helmet to manipulate scripts and styles programatically 2024-06-12 13:03:19 +02:00
Athou
cd50b6b058 use new playwright locators 2024-06-12 11:39:51 +02:00
Athou
b0c7ef18db fix test race condition 2024-06-12 10:30:47 +02:00
Athou
24171faf86 fetchFeedInternal follows redirects, we don't need to call it twice (#1431) 2024-06-12 08:21:11 +02:00
Jérémie Panzer
941f14dd41 Merge pull request #1374 from Athou/dependabot/npm_and_yarn/commafeed-client/eslint-plugin-react-hooks-4.6.2
Bump eslint-plugin-react-hooks from 4.6.0 to 4.6.2 in /commafeed-client
2024-06-11 08:37:23 +02:00
Jérémie Panzer
d46ef787db Merge pull request #1404 from Athou/dependabot/npm_and_yarn/commafeed-client/vitest-1.6.0
Bump vitest from 1.5.0 to 1.6.0 in /commafeed-client
2024-06-11 08:37:10 +02:00
Jérémie Panzer
ec7447a38c Merge pull request #1411 from Athou/dependabot/npm_and_yarn/commafeed-client/react-router-dom-6.23.1
Bump react-router-dom from 6.22.3 to 6.23.1 in /commafeed-client
2024-06-11 08:33:22 +02:00
dependabot[bot]
2a3fc3ae15 Bump eslint-plugin-react-hooks from 4.6.0 to 4.6.2 in /commafeed-client
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 4.6.0 to 4.6.2.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 06:31:59 +00:00
dependabot[bot]
ef25582bcb Bump react-router-dom from 6.22.3 to 6.23.1 in /commafeed-client
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.22.3 to 6.23.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.23.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 06:29:29 +00:00
Jérémie Panzer
55bbb2542d Merge pull request #1402 from Athou/dependabot/npm_and_yarn/commafeed-client/axios-1.7.2
Bump axios from 1.6.8 to 1.7.2 in /commafeed-client
2024-06-11 08:28:51 +02:00
Jérémie Panzer
8e94ac74a8 Merge pull request #1401 from Athou/dependabot/npm_and_yarn/commafeed-client/react-redux-9.1.2
Bump react-redux from 9.1.0 to 9.1.2 in /commafeed-client
2024-06-11 08:28:42 +02:00
Jérémie Panzer
90ecb9253c Merge pull request #1416 from Athou/dependabot/npm_and_yarn/commafeed-client/eslint-plugin-react-7.34.2
Bump eslint-plugin-react from 7.34.1 to 7.34.2 in /commafeed-client
2024-06-11 08:28:26 +02:00
Jérémie Panzer
6721842d98 Merge pull request #1414 from Athou/dependabot/npm_and_yarn/commafeed-client/lingui-240f3c17ef
Bump the lingui group across 1 directory with 5 updates
2024-06-11 08:28:15 +02:00
Jérémie Panzer
8b487ec414 Merge pull request #1428 from Athou/dependabot/npm_and_yarn/commafeed-client/typescript-eslint/eslint-plugin-7.13.0
Bump @typescript-eslint/eslint-plugin from 7.6.0 to 7.13.0 in /commafeed-client
2024-06-11 08:28:03 +02:00
dependabot[bot]
d6382861c3 Bump the lingui group across 1 directory with 5 updates
Bumps the lingui group with 5 updates in the /commafeed-client directory:

| Package | From | To |
| --- | --- | --- |
| [@lingui/core](https://github.com/lingui/js-lingui) | `4.10.0` | `4.11.1` |
| [@lingui/macro](https://github.com/lingui/js-lingui) | `4.10.0` | `4.11.1` |
| [@lingui/react](https://github.com/lingui/js-lingui) | `4.10.0` | `4.11.1` |
| [@lingui/cli](https://github.com/lingui/js-lingui) | `4.10.0` | `4.11.1` |
| [@lingui/vite-plugin](https://github.com/lingui/js-lingui) | `4.10.0` | `4.11.1` |



Updates `@lingui/core` from 4.10.0 to 4.11.1
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v4.10.0...v4.11.1)

Updates `@lingui/macro` from 4.10.0 to 4.11.1
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v4.10.0...v4.11.1)

Updates `@lingui/react` from 4.10.0 to 4.11.1
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v4.10.0...v4.11.1)

Updates `@lingui/cli` from 4.10.0 to 4.11.1
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v4.10.0...v4.11.1)

Updates `@lingui/vite-plugin` from 4.10.0 to 4.11.1
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v4.10.0...v4.11.1)

---
updated-dependencies:
- dependency-name: "@lingui/core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: lingui
- dependency-name: "@lingui/macro"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: lingui
- dependency-name: "@lingui/react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: lingui
- dependency-name: "@lingui/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: lingui
- dependency-name: "@lingui/vite-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: lingui
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:27:42 +00:00
dependabot[bot]
2cdea99a69 Bump axios from 1.6.8 to 1.7.2 in /commafeed-client
Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.8...v1.7.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:26:35 +00:00
dependabot[bot]
b1ae1c8afd Bump react-redux from 9.1.0 to 9.1.2 in /commafeed-client
Bumps [react-redux](https://github.com/reduxjs/react-redux) from 9.1.0 to 9.1.2.
- [Release notes](https://github.com/reduxjs/react-redux/releases)
- [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reduxjs/react-redux/compare/v9.1.0...v9.1.2)

---
updated-dependencies:
- dependency-name: react-redux
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:26:07 +00:00
Jérémie Panzer
c09cd0c717 Merge pull request #1379 from Athou/dependabot/npm_and_yarn/commafeed-client/dayjs-1.11.11
Bump dayjs from 1.11.10 to 1.11.11 in /commafeed-client
2024-06-11 07:26:00 +02:00
Jérémie Panzer
f50e0ae272 Merge pull request #1375 from Athou/dependabot/npm_and_yarn/commafeed-client/multi-ec30208de6
Bump react-dom and @types/react-dom in /commafeed-client
2024-06-11 07:25:51 +02:00
Jérémie Panzer
b99b91a2a8 Merge pull request #1384 from Athou/dependabot/npm_and_yarn/commafeed-client/tss-react-4.9.10
Bump tss-react from 4.9.6 to 4.9.10 in /commafeed-client
2024-06-11 07:25:28 +02:00
Jérémie Panzer
d9759de6f1 Merge pull request #1403 from Athou/dependabot/npm_and_yarn/commafeed-client/monaco-editor-0.49.0
Bump monaco-editor from 0.47.0 to 0.49.0 in /commafeed-client
2024-06-11 07:25:19 +02:00
dependabot[bot]
cf2b7f9e4f Bump eslint-plugin-react from 7.34.1 to 7.34.2 in /commafeed-client
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.34.1 to 7.34.2.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.34.1...v7.34.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:25:14 +00:00
Jérémie Panzer
ee880c06ed Merge pull request #1407 from Athou/dependabot/npm_and_yarn/commafeed-client/react-icons-5.2.1
Bump react-icons from 5.0.1 to 5.2.1 in /commafeed-client
2024-06-11 07:25:10 +02:00
Jérémie Panzer
bc2e13ef22 Merge pull request #1410 from Athou/dependabot/npm_and_yarn/commafeed-client/reduxjs/toolkit-2.2.5
Bump @reduxjs/toolkit from 2.2.3 to 2.2.5 in /commafeed-client
2024-06-11 07:25:01 +02:00
dependabot[bot]
39ecfe2782 Bump @typescript-eslint/eslint-plugin in /commafeed-client
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 7.6.0 to 7.13.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.13.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:24:54 +00:00
Jérémie Panzer
3295d82f69 Merge pull request #1371 from Athou/dependabot/npm_and_yarn/commafeed-client/fontsource/open-sans-5.0.28
Bump @fontsource/open-sans from 5.0.27 to 5.0.28 in /commafeed-client
2024-06-11 07:24:42 +02:00
dependabot[bot]
1cd27a59e2 Bump vitest from 1.5.0 to 1.6.0 in /commafeed-client
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.0/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:24:35 +00:00
Jérémie Panzer
e1602edff1 Merge pull request #1426 from Athou/dependabot/npm_and_yarn/commafeed-client/redoc-2.1.5
Bump redoc from 2.1.3 to 2.1.5 in /commafeed-client
2024-06-11 07:24:03 +02:00
Jérémie Panzer
ef8e61d6fc Merge pull request #1427 from Athou/dependabot/npm_and_yarn/commafeed-client/prettier-3.3.1
Bump prettier from 3.2.5 to 3.3.1 in /commafeed-client
2024-06-11 07:23:54 +02:00
Jérémie Panzer
0057030442 Merge pull request #1429 from Athou/dependabot/npm_and_yarn/commafeed-client/vite-5.2.13
Bump vite from 5.2.8 to 5.2.13 in /commafeed-client
2024-06-11 07:23:33 +02:00
Jérémie Panzer
6fabe46d6e Merge pull request #1430 from Athou/dependabot/npm_and_yarn/commafeed-client/vitejs/plugin-react-4.3.1
Bump @vitejs/plugin-react from 4.2.1 to 4.3.1 in /commafeed-client
2024-06-11 07:23:21 +02:00
dependabot[bot]
37c58f2755 Bump monaco-editor from 0.47.0 to 0.49.0 in /commafeed-client
Bumps [monaco-editor](https://github.com/microsoft/monaco-editor) from 0.47.0 to 0.49.0.
- [Release notes](https://github.com/microsoft/monaco-editor/releases)
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.47.0...v0.49.0)

---
updated-dependencies:
- dependency-name: monaco-editor
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:18:41 +00:00
dependabot[bot]
bb982c3caf Bump @reduxjs/toolkit from 2.2.3 to 2.2.5 in /commafeed-client
Bumps [@reduxjs/toolkit](https://github.com/reduxjs/redux-toolkit) from 2.2.3 to 2.2.5.
- [Release notes](https://github.com/reduxjs/redux-toolkit/releases)
- [Commits](https://github.com/reduxjs/redux-toolkit/compare/v2.2.3...v2.2.5)

---
updated-dependencies:
- dependency-name: "@reduxjs/toolkit"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:18:39 +00:00
Jérémie Panzer
7e4c3737a8 Merge pull request #1415 from Athou/dependabot/npm_and_yarn/commafeed-client/mantine-1f5c67be2f
Bump the mantine group across 1 directory with 6 updates
2024-06-11 07:17:44 +02:00
dependabot[bot]
23596b5ac6 Bump @vitejs/plugin-react from 4.2.1 to 4.3.1 in /commafeed-client
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:12:52 +00:00
dependabot[bot]
2fdeb7acd8 Bump vite from 5.2.8 to 5.2.13 in /commafeed-client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.8 to 5.2.13.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.2.13/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.2.13/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:12:29 +00:00
dependabot[bot]
c62cac478c Bump the mantine group across 1 directory with 6 updates
Bumps the mantine group with 6 updates in the /commafeed-client directory:

| Package | From | To |
| --- | --- | --- |
| [@mantine/core](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/core) | `7.8.0` | `7.10.1` |
| [@mantine/form](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/form) | `7.8.0` | `7.10.1` |
| [@mantine/hooks](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks) | `7.8.0` | `7.10.1` |
| [@mantine/modals](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/modals) | `7.8.0` | `7.10.1` |
| [@mantine/notifications](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/notifications) | `7.8.0` | `7.10.1` |
| [@mantine/spotlight](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/spotlight) | `7.8.0` | `7.10.1` |



Updates `@mantine/core` from 7.8.0 to 7.10.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.10.1/packages/@mantine/core)

Updates `@mantine/form` from 7.8.0 to 7.10.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.10.1/packages/@mantine/form)

Updates `@mantine/hooks` from 7.8.0 to 7.10.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.10.1/packages/@mantine/hooks)

Updates `@mantine/modals` from 7.8.0 to 7.10.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.10.1/packages/@mantine/modals)

Updates `@mantine/notifications` from 7.8.0 to 7.10.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.10.1/packages/@mantine/notifications)

Updates `@mantine/spotlight` from 7.8.0 to 7.10.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.10.1/packages/@mantine/spotlight)

---
updated-dependencies:
- dependency-name: "@mantine/core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/form"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/hooks"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/modals"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/notifications"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/spotlight"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:12:05 +00:00
dependabot[bot]
e9026e0371 Bump prettier from 3.2.5 to 3.3.1 in /commafeed-client
Bumps [prettier](https://github.com/prettier/prettier) from 3.2.5 to 3.3.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.5...3.3.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:11:50 +00:00
dependabot[bot]
7446d906ae Bump redoc from 2.1.3 to 2.1.5 in /commafeed-client
Bumps [redoc](https://github.com/Redocly/redoc) from 2.1.3 to 2.1.5.
- [Release notes](https://github.com/Redocly/redoc/releases)
- [Changelog](https://github.com/Redocly/redoc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Redocly/redoc/compare/v2.1.3...v2.1.5)

---
updated-dependencies:
- dependency-name: redoc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:11:42 +00:00
Jérémie Panzer
62ad09ac93 Merge pull request #1358 from Athou/dependabot/maven/com.ibm.icu-icu4j-75.1
Bump com.ibm.icu:icu4j from 74.2 to 75.1
2024-06-11 07:10:33 +02:00
Jérémie Panzer
01d1f920a8 Merge pull request #1391 from Athou/dependabot/maven/com.microsoft.playwright-playwright-1.44.0
Bump com.microsoft.playwright:playwright from 1.43.0 to 1.44.0
2024-06-11 07:10:25 +02:00
Jérémie Panzer
057810470c Merge pull request #1392 from Athou/dependabot/maven/io.swagger.core.v3-swagger-annotations-2.2.22
Bump io.swagger.core.v3:swagger-annotations from 2.2.21 to 2.2.22
2024-06-11 07:10:18 +02:00
Jérémie Panzer
5a6d6be8e5 Merge pull request #1394 from Athou/dependabot/maven/com.google.apis-google-api-services-youtube-v3-rev20240514-2.0.0
Bump com.google.apis:google-api-services-youtube from v3-rev20240310-2.0.0 to v3-rev20240514-2.0.0
2024-06-11 07:10:09 +02:00
Jérémie Panzer
c6c813a4ee Merge pull request #1395 from Athou/dependabot/maven/io.swagger.core.v3-swagger-maven-plugin-jakarta-2.2.22
Bump io.swagger.core.v3:swagger-maven-plugin-jakarta from 2.2.21 to 2.2.22
2024-06-11 07:10:01 +02:00
Jérémie Panzer
ad5787a38b Merge pull request #1421 from Athou/dependabot/maven/io.github.hakky54-sslcontext-kickstart-for-apache5-8.3.6
Bump io.github.hakky54:sslcontext-kickstart-for-apache5 from 8.3.4 to 8.3.6
2024-06-11 07:09:53 +02:00
Jérémie Panzer
387ceabf30 Merge pull request #1418 from Athou/dependabot/maven/org.apache.maven.plugins-maven-shade-plugin-3.6.0
Bump org.apache.maven.plugins:maven-shade-plugin from 3.5.2 to 3.6.0
2024-06-11 07:09:32 +02:00
Jérémie Panzer
ffe6962c36 Merge pull request #1423 from Athou/dependabot/maven/io.github.git-commit-id-git-commit-id-maven-plugin-9.0.0
Bump io.github.git-commit-id:git-commit-id-maven-plugin from 8.0.2 to 9.0.0
2024-06-11 07:09:24 +02:00
Jérémie Panzer
6d599fc77d Merge pull request #1422 from Athou/dependabot/maven/org.apache.maven.plugins-maven-checkstyle-plugin-3.4.0
Bump org.apache.maven.plugins:maven-checkstyle-plugin from 3.3.1 to 3.4.0
2024-06-11 07:09:01 +02:00
Jérémie Panzer
9fcff1342c Merge pull request #1425 from Athou/dependabot/npm_and_yarn/commafeed-client/braces-3.0.3
Bump braces from 3.0.2 to 3.0.3 in /commafeed-client
2024-06-11 07:08:01 +02:00
dependabot[bot]
f7dbc2e9aa Bump braces from 3.0.2 to 3.0.3 in /commafeed-client
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 05:03:52 +00:00
Athou
468f2e4c76 remove warnings 2024-06-11 07:02:38 +02:00
Athou
883c9c79aa replace source and target with the new release setting 2024-06-10 12:20:26 +02:00
Athou
f171d05088 querydsl is no longer maintained, use an active fork 2024-06-10 12:17:36 +02:00
dependabot[bot]
f85745fe40 Bump io.github.git-commit-id:git-commit-id-maven-plugin
Bumps [io.github.git-commit-id:git-commit-id-maven-plugin](https://github.com/git-commit-id/git-commit-id-maven-plugin) from 8.0.2 to 9.0.0.
- [Release notes](https://github.com/git-commit-id/git-commit-id-maven-plugin/releases)
- [Commits](https://github.com/git-commit-id/git-commit-id-maven-plugin/compare/v8.0.2...v9.0.0)

---
updated-dependencies:
- dependency-name: io.github.git-commit-id:git-commit-id-maven-plugin
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 09:17:15 +00:00
Athou
5ad93bb3ba add more checkstyle rules 2024-06-10 07:40:35 +02:00
dependabot[bot]
d80ed9d4dd Bump org.apache.maven.plugins:maven-checkstyle-plugin
Bumps [org.apache.maven.plugins:maven-checkstyle-plugin](https://github.com/apache/maven-checkstyle-plugin) from 3.3.1 to 3.4.0.
- [Commits](https://github.com/apache/maven-checkstyle-plugin/compare/maven-checkstyle-plugin-3.3.1...maven-checkstyle-plugin-3.4.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-checkstyle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-08 20:35:05 +00:00
dependabot[bot]
69b5f5418a Bump io.github.hakky54:sslcontext-kickstart-for-apache5
Bumps [io.github.hakky54:sslcontext-kickstart-for-apache5](https://github.com/Hakky54/sslcontext-kickstart) from 8.3.4 to 8.3.6.
- [Changelog](https://github.com/Hakky54/sslcontext-kickstart/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Hakky54/sslcontext-kickstart/compare/v8.3.4...v8.3.6)

---
updated-dependencies:
- dependency-name: io.github.hakky54:sslcontext-kickstart-for-apache5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-08 20:35:01 +00:00
dependabot[bot]
06aa37659c Bump org.apache.maven.plugins:maven-shade-plugin from 3.5.2 to 3.6.0
Bumps [org.apache.maven.plugins:maven-shade-plugin](https://github.com/apache/maven-shade-plugin) from 3.5.2 to 3.6.0.
- [Release notes](https://github.com/apache/maven-shade-plugin/releases)
- [Commits](https://github.com/apache/maven-shade-plugin/compare/maven-shade-plugin-3.5.2...maven-shade-plugin-3.6.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-shade-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 09:45:39 +00:00
Jérémie Panzer
d5c98de839 Merge pull request #1360 from Athou/dependabot/maven/org.apache.maven.plugins-maven-jar-plugin-3.4.1
Bump org.apache.maven.plugins:maven-jar-plugin from 3.4.0 to 3.4.1
2024-06-02 10:19:14 +02:00
Jérémie Panzer
920975059c Merge pull request #1393 from Athou/dependabot/maven/org.mariadb.jdbc-mariadb-java-client-3.4.0
Bump org.mariadb.jdbc:mariadb-java-client from 3.3.3 to 3.4.0
2024-06-02 10:18:07 +02:00
Jérémie Panzer
7c6e4c3356 Merge pull request #1397 from Athou/dependabot/maven/redis.clients-jedis-5.1.3
Bump redis.clients:jedis from 5.1.2 to 5.1.3
2024-06-02 10:17:46 +02:00
Jérémie Panzer
143971da5e Merge pull request #1385 from Athou/dependabot/maven/com.mysql-mysql-connector-j-8.4.0
Bump com.mysql:mysql-connector-j from 8.3.0 to 8.4.0
2024-06-02 10:17:32 +02:00
dependabot[bot]
8976e9c01a Bump react-icons from 5.0.1 to 5.2.1 in /commafeed-client
Bumps [react-icons](https://github.com/react-icons/react-icons) from 5.0.1 to 5.2.1.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v5.0.1...v5.2.1)

---
updated-dependencies:
- dependency-name: react-icons
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 07:10:59 +00:00
dependabot[bot]
20c6355efd Bump redis.clients:jedis from 5.1.2 to 5.1.3
Bumps [redis.clients:jedis](https://github.com/redis/jedis) from 5.1.2 to 5.1.3.
- [Release notes](https://github.com/redis/jedis/releases)
- [Commits](https://github.com/redis/jedis/compare/v5.1.2...v5.1.3)

---
updated-dependencies:
- dependency-name: redis.clients:jedis
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 07:06:21 +00:00
Jérémie Panzer
f86f38ef7a Merge pull request #1396 from SConaway/patch-1
login page: don't autocapitalize the username input
2024-05-26 10:14:36 +02:00
Jérémie Panzer
24311df551 Merge pull request #1389 from luckrnx09/luckrnx09-cmd-k-for-macos-users
Maint: Show `Cmd + K` for macOS users
2024-05-26 10:14:10 +02:00
luckrnx09
d02aa78def fix: determine os type from useOs hook 2024-05-25 21:35:10 +08:00
Steven Conaway
b131020f46 login page: don't autocapitalize the username input 2024-05-22 10:37:32 -06:00
dependabot[bot]
4ab82782b0 Bump io.swagger.core.v3:swagger-maven-plugin-jakarta
Bumps io.swagger.core.v3:swagger-maven-plugin-jakarta from 2.2.21 to 2.2.22.

---
updated-dependencies:
- dependency-name: io.swagger.core.v3:swagger-maven-plugin-jakarta
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 09:56:18 +00:00
dependabot[bot]
6f9ebd5d78 Bump com.google.apis:google-api-services-youtube
Bumps com.google.apis:google-api-services-youtube from v3-rev20240310-2.0.0 to v3-rev20240514-2.0.0.

---
updated-dependencies:
- dependency-name: com.google.apis:google-api-services-youtube
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 09:56:15 +00:00
dependabot[bot]
7ebbf26369 Bump org.mariadb.jdbc:mariadb-java-client from 3.3.3 to 3.4.0
Bumps [org.mariadb.jdbc:mariadb-java-client](https://github.com/mariadb-corporation/mariadb-connector-j) from 3.3.3 to 3.4.0.
- [Release notes](https://github.com/mariadb-corporation/mariadb-connector-j/releases)
- [Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.3.3...3.4.0)

---
updated-dependencies:
- dependency-name: org.mariadb.jdbc:mariadb-java-client
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 09:55:10 +00:00
dependabot[bot]
dbc93f9928 Bump io.swagger.core.v3:swagger-annotations from 2.2.21 to 2.2.22
Bumps io.swagger.core.v3:swagger-annotations from 2.2.21 to 2.2.22.

---
updated-dependencies:
- dependency-name: io.swagger.core.v3:swagger-annotations
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 09:55:06 +00:00
dependabot[bot]
ad6ebd7e4d Bump com.microsoft.playwright:playwright from 1.43.0 to 1.44.0
Bumps [com.microsoft.playwright:playwright](https://github.com/microsoft/playwright-java) from 1.43.0 to 1.44.0.
- [Release notes](https://github.com/microsoft/playwright-java/releases)
- [Commits](https://github.com/microsoft/playwright-java/compare/v1.43.0...v1.44.0)

---
updated-dependencies:
- dependency-name: com.microsoft.playwright:playwright
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 09:55:04 +00:00
luckrnx09
ab86247c8c update the hot key to mod+k for call spotlight 2024-05-15 21:47:34 +08:00
luckrnx09
884516be28 Maint: Show Cmd + K for macOS users 2024-05-15 21:42:32 +08:00
dependabot[bot]
c236b1adda Bump com.mysql:mysql-connector-j from 8.3.0 to 8.4.0
Bumps [com.mysql:mysql-connector-j](https://github.com/mysql/mysql-connector-j) from 8.3.0 to 8.4.0.
- [Changelog](https://github.com/mysql/mysql-connector-j/blob/release/8.x/CHANGES)
- [Commits](https://github.com/mysql/mysql-connector-j/compare/8.3.0...8.4.0)

---
updated-dependencies:
- dependency-name: com.mysql:mysql-connector-j
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 09:42:14 +00:00
dependabot[bot]
222117dafe Bump tss-react from 4.9.6 to 4.9.10 in /commafeed-client
Bumps [tss-react](https://github.com/garronej/tss-react) from 4.9.6 to 4.9.10.
- [Release notes](https://github.com/garronej/tss-react/releases)
- [Commits](https://github.com/garronej/tss-react/compare/v4.9.6...v4.9.10)

---
updated-dependencies:
- dependency-name: tss-react
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 09:22:50 +00:00
dependabot[bot]
38cd27df57 Bump dayjs from 1.11.10 to 1.11.11 in /commafeed-client
Bumps [dayjs](https://github.com/iamkun/dayjs) from 1.11.10 to 1.11.11.
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.10...v1.11.11)

---
updated-dependencies:
- dependency-name: dayjs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 15:58:08 +00:00
dependabot[bot]
5e07e74bb2 Bump react-dom and @types/react-dom in /commafeed-client
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). These dependencies needed to be updated together.

Updates `react-dom` from 18.2.0 to 18.3.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/react-dom)

Updates `@types/react-dom` from 18.2.25 to 18.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-26 19:04:55 +00:00
dependabot[bot]
fe779e361f Bump @fontsource/open-sans from 5.0.27 to 5.0.28 in /commafeed-client
Bumps [@fontsource/open-sans](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/open-sans) from 5.0.27 to 5.0.28.
- [Changelog](https://github.com/fontsource/font-files/blob/main/fonts/google/open-sans/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/open-sans)

---
updated-dependencies:
- dependency-name: "@fontsource/open-sans"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-26 19:03:47 +00:00
dependabot[bot]
1a51799497 Bump org.apache.maven.plugins:maven-jar-plugin from 3.4.0 to 3.4.1
Bumps [org.apache.maven.plugins:maven-jar-plugin](https://github.com/apache/maven-jar-plugin) from 3.4.0 to 3.4.1.
- [Release notes](https://github.com/apache/maven-jar-plugin/releases)
- [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.4.0...maven-jar-plugin-3.4.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-jar-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 09:47:58 +00:00
dependabot[bot]
6ea926cdb0 Bump com.ibm.icu:icu4j from 74.2 to 75.1
Bumps [com.ibm.icu:icu4j](https://github.com/unicode-org/icu) from 74.2 to 75.1.
- [Release notes](https://github.com/unicode-org/icu/releases)
- [Commits](https://github.com/unicode-org/icu/commits)

---
updated-dependencies:
- dependency-name: com.ibm.icu:icu4j
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 09:46:50 +00:00
Jérémie Panzer
439d61946a Merge pull request #1351 from WangLei1993/master
add Chinese translation for new entry
2024-04-16 10:20:52 +02:00
WangLei1993
426c8d7dfb add Chinese translation for new entry 2024-04-16 15:01:23 +08:00
Athou
f1b51e8342 set the default value for new users to the same value as the default value for existing users 2024-04-15 21:59:54 +02:00
405 changed files with 40055 additions and 37134 deletions

View File

@@ -1,6 +1 @@
# ignore everything commafeed-client
*
# allow only what we need
!commafeed-server/target/commafeed.jar
!commafeed-server/config.yml.example

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
* text eol=lf
*.cmd text eol=crlf
*.png binary

7
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,5 @@
github: [athou] github: [ athou ]
custom: ['https://www.paypal.com/donate/?business=9CNQHMJG2ZJVY&no_recurring=0&item_name=CommaFeed&currency_code=EUR'] custom: [
'https://github.com/sponsors/Athou',
'https://www.paypal.com/donate/?business=9CNQHMJG2ZJVY&no_recurring=0&item_name=CommaFeed&currency_code=EUR'
]

View File

@@ -25,7 +25,8 @@ If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):** **Environment (please complete the following information):**
- CommaFeed version (or "commafeed.com"): 3.2.1 - commafeed.com or self-hosted:
- If self-hosted, CommaFeed version [e.g. 5.1.0 (a3dcb2c)]:
- Browser [e.g. chrome, firefox]: - Browser [e.g. chrome, firefox]:
- Device [e.g. desktop, mobile]: - Device [e.g. desktop, mobile]:

View File

@@ -1,24 +0,0 @@
version: 2
updates:
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 50
- package-ecosystem: "npm"
directory: "/commafeed-client"
schedule:
interval: "monthly"
open-pull-requests-limit: 50
groups:
mantine:
patterns:
- "@mantine/*"
lingui:
patterns:
- "@lingui/*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

36
.github/stale.yml vendored
View File

@@ -1,19 +1,19 @@
# Number of days of inactivity before an issue becomes stale # Number of days of inactivity before an issue becomes stale
daysUntilStale: 60 daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed # Number of days of inactivity before a stale issue is closed
daysUntilClose: 7 daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- pinned - pinned
- security - security
- enhancement - enhancement
- bug - bug
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: wontfix staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable
markComment: > markComment: >
This issue has been automatically marked as stale because it has not had This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you recent activity. It will be closed if no further activity occurs. Thank you
for your contributions. for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable # Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false closeComment: false

View File

@@ -1,89 +0,0 @@
name: Java CI
on: [ push ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ "17", "21" ]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: "temurin"
cache: "maven"
# Build
- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify
- name: Upload JAR
uses: actions/upload-artifact@v4
if: ${{ matrix.java == '17' }}
with:
name: commafeed.jar
path: commafeed-server/target/commafeed.jar
# Docker
- name: Login to Container Registry
uses: docker/login-action@v3
if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker build and push tag
uses: docker/build-push-action@v5
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
tags: |
athou/commafeed:latest
athou/commafeed:${{ github.ref_name }}
- name: Docker build and push master
uses: docker/build-push-action@v5
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
tags: athou/commafeed:master
# Create GitHub release after Docker image has been published
- name: Extract Changelog Entry
uses: mindsers/changelog-reader-action@v2
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
id: changelog_reader
with:
version: ${{ github.ref_name }}
- name: Create GitHub release
uses: softprops/action-gh-release@v2
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with:
name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }}
draft: false
prerelease: false
files: |
commafeed-server/target/commafeed.jar
commafeed-server/config.yml.example

278
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,278 @@
name: ci
permissions:
contents: read
on:
push:
pull_request:
env:
JAVA_VERSION: 21
DOCKER_BUILD_SUMMARY: false
jobs:
build:
if: github.event_name != 'pull_request' || github.actor != 'renovate[bot]' # renovate already triggers the build on pushes
strategy:
matrix:
os: [ "ubuntu-latest", "ubuntu-22.04-arm", "windows-latest" ]
database: [ "h2", "postgresql", "mysql", "mariadb" ]
runs-on: ${{ matrix.os }}
steps:
# Checkout
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
# Setup
- name: Set up GraalVM
uses: graalvm/setup-graalvm@01ed653ac833fe80569f1ef9f25585ba2811baab # v1
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm"
cache: "maven"
- name: Install Playwright dependencies
run: sudo apt-get install -y libgbm1
if: matrix.os != 'windows-latest'
# Build & Test
- name: Build with Maven
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
# Build pages
- name: Create pages directory structure
run: mkdir -p target/pages/documentation/custom-css
- name: Convert readme file to html
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
with:
source: README.md
output: target/pages/index.html
- name: Convert config documentation to html
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
- name: Upload cross-platform app
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
with:
name: commafeed-${{ matrix.database }}-jvm
path: commafeed-server/target/commafeed-*.zip
- name: Upload native executable
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
path: commafeed-server/target/commafeed-*-runner*
- name: Upload pages
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
if: matrix.os == 'ubuntu-latest' && matrix.database == 'h2' # we only need to upload the pages once
with:
path: target/pages
docker:
runs-on: ubuntu-latest
needs: build
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
strategy:
matrix:
database: [ "h2", "postgresql", "mysql", "mariadb" ]
steps:
# Checkout
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Install required packages
run: sudo apt-get install -y rename unzip
# Prepare artifacts
- name: Download artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: commafeed-${{ matrix.database }}-*
path: ./artifacts
merge-multiple: true
- name: Set the exec flag on the native executables
run: chmod +x artifacts/*-runner
- name: Rename native executables to match buildx TARGETARCH
run: |
rename 's/x86_64/amd64/g' artifacts/*
rename 's/aarch_64/arm64/g' artifacts/*
- name: Unzip jvm package
run: |
unzip artifacts/*-jvm.zip -d artifacts/extracted-jvm-package
rename 's/commafeed-.*/quarkus-app/g' artifacts/extracted-jvm-package/*
# Docker
- name: Login to Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ env.DOCKERHUB_USERNAME != '' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
## build but don't push for PRs and renovate
- name: Docker build - native
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
push: false
platforms: linux/amd64,linux/arm64/v8
- name: Docker build - jvm
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
push: false
platforms: linux/amd64,linux/arm64/v8
## build and push tag
- name: Docker build and push tag - native
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
push: ${{ env.DOCKERHUB_USERNAME != '' }}
platforms: linux/amd64,linux/arm64/v8
tags: |
athou/commafeed:latest-${{ matrix.database }}
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
- name: Docker build and push tag - jvm
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
push: ${{ env.DOCKERHUB_USERNAME != '' }}
platforms: linux/amd64,linux/arm64/v8
tags: |
athou/commafeed:latest-${{ matrix.database }}-jvm
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
## build and push master
- name: Docker build and push master - native
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_name == 'master' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
push: ${{ env.DOCKERHUB_USERNAME != '' }}
platforms: linux/amd64,linux/arm64/v8
tags: athou/commafeed:master-${{ matrix.database }}
- name: Docker build and push master - jvm
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_name == 'master' }}
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
push: ${{ env.DOCKERHUB_USERNAME != '' }}
platforms: linux/amd64,linux/arm64/v8
tags: athou/commafeed:master-${{ matrix.database }}-jvm
release:
runs-on: ubuntu-latest
needs:
- build
- docker
permissions:
contents: write
if: github.ref_type == 'tag'
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: commafeed-*
path: ./artifacts
merge-multiple: true
- name: Set the exec flag on the native executables
run: chmod +x artifacts/*-runner
- name: Extract Changelog Entry
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2
id: changelog_reader
with:
version: ${{ github.ref_name }}
- name: Create GitHub release
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1
with:
name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }}
artifacts: ./artifacts/*
update-dockerhub-description:
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Update Docker Hub Description
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: athou/commafeed
short-description: ${{ github.event.repository.description }}
readme-filepath: commafeed-server/src/main/docker/README.md
deploy-pages:
runs-on: ubuntu-latest
needs: release
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
id: deployment

Binary file not shown.

View File

@@ -1,18 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one # Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file # or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information # distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file # regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the # to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance # "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at # with the License. You may obtain a copy of the License at
# #
# https://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, # Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an # software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the # KIND, either express or implied. See the License for the
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip distributionType=only-script
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

View File

@@ -1,329 +1,497 @@
# Changelog # Changelog
## [4.4.0] ## [5.10.0]
- add support for sharing using the browser native capabilities if available (#1255) - 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)
- add a button in the entry headers to star an entry (#1025) - Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again
- add a button in the entry headers to open links in a new tab (#1333) - The aarch64 native executable now also works on the Raspberry Pi 5 (#1795)
- add two options in the settings to toggle those buttons - Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are displayed (#1087)
- accept .opml file extension when importing and export with the .opml extension
- the "mark as read" option is no longer shown in the context menu for entries that are too old to be marked as read ( ## [5.9.0]
older than `keepStatusDays`) (#1303)
- A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757)
## [4.3.3] - 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)
- fix OPML import (#1279)
## [5.8.0]
## [4.3.2]
- A color picker is now available on the settings page to change the orange accent of the application (#1598)
- added support for unix sockets (#1278) - A font size slider is now available to change the size of the text of feed entries (#1462)
- The "mark all as read" confirmation setting now also applies to the "shift+a" keyboard shortcut (#1744)
## [4.3.1] - CommaFeed wil try to match the language of the browser before defaulting to english (#1767)
- The default value for the number of entries to keep above the selected entry when scrolling is now 1 instead of 0 to match what other feed readers do
- fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database and the database
timezone is not UTC (#1239) ## [5.7.0]
- videos in enclosures can no longer have a width larger than the page (#1240)
- Add Shift+J/Shift+K keyboard shortcuts to navigate to the next/previous feed or category with unread entries (#1558)
## [4.3.0] - Add the referrer "no-referrer" meta to index.html (#1724)
- Load custom JS code when the app is done loading (#1724)
- h2 (the embedded database) has been upgraded to 2.2.224 - Correctly handle feeds that return an unmodified Last-Modified header but a different ETag header (#1746)
- this version uses a different file format than 2.1.x, the first time you start CommaFeed with this version, the - Restore gzip compression of responses that was accidentaly disabled since 5.0.0
database will be automatically converted to the new format - Fix tooltips not showing up in mobile view
- add a setting to completely disable scrolling to selected entry (#1157) - Fix the bookmarklet generator on the About page
- add a css class reflecting the current view mode to ease custom css rules (#1232)
- fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database (#1239) ## [5.6.1]
## [4.2.1] - Restore support for iframes in feed entries (#1688)
- There is now a package available for Arch Linux thanks to @dcelasun (#1691)
- fix an issue that caused the tree to show an incorrect unread count after a websocket notification because entries
that were already marked as read by a filtering expression were not ignored (#1191) ## [5.6.0]
## [4.2.0] - To better respect the bandwidth of feed owners, the default value of `commafeed.feed-refresh.interval-empirical` is now true. This means feeds no longer refresh exactly every 5 minutes (the default value of `commafeed.feed-refresh.interval`) but between 5 minutes and 4 hours (the default value of the new `commafeed.feed-refresh.max-interval` setting). The interval is calculated based on feed activity, so highly active feeds refresh more often (#1677)
- Many previously hardcoded values used in feed refresh interval calculation are now exposed as settings (#1677)
- add a setting to display the action buttons in the footer instead of in the header on mobile (#1121) - Access to local addresses is now blocked to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal resources. You might want to disable the new `commafeed.http-client.block-local-addresses` setting if you subscribe to feeds only available on your local network and you trust all your users
- the websocket notification now contains everything needed to update the UI, the client no longer needs to make an API - If a feed responds with a "429 - Too many requests" response, a backoff mechanism is triggered when the response does not contain a "Retry-After" header
call to get the latest data when receiving the notification
- add a workaround to the Fever API for the Unread iOS app (#1188) ## [5.5.0]
- fix an issue that caused dates to be saved incorrectly if the database server and the application server were in
different timezones (#1187) - CommaFeed now honors the Retry-After response header and will not try to refresh a feed sooner than the value of this header
- Audio enclosures (e.g. podcasts) now fill available entry width
## [4.1.0] - Fix an issue with some labels not correctly internationalized
- it is now possible to open the sidebar on mobile by swiping to the right (#1098) ## [5.4.0]
- swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar
- the full hierarchy of categories are now displayed in the category dropdown (#1045) - An arm64 native executable is now available for download on the releases page
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup. - The native executable Docker image now supports arm64
The setting is disabled by default for existing installations, except for the docker image where it is enabled and set - Fixed an issue with feeds that declared an invalid DOCTYPE (#1260)
to 365 days
- if user registrations are disabled on your instance which is the default behavior, users are redirected on the login ## [5.3.6]
page instead of the welcome page when not logged in (#1185)
- the sidebar resizer is no longer shown in the middle of the screen on mobile - Ignore invalid Cache-Control header values (#1619)
- when using the system color scheme and the system is using a dark theme, feed entries no longer flicker on load
- the demo account (if enabled) cannot register custom javascript code anymore ## [5.3.5]
- removed the usage of `toSorted` in the client because older browsers do not support it (#1183)
- the openapi documentation is no longer cached by the browser so you always have access to the latest version - Fixed an issue with the aspect ratio of images of some feeds (#1595)
- added a memory management section to the readme, reading it is recommended if you are running CommaFeed on a server - CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age property (#1615)
with limited memory - Added support for compilation with JDK 23+. If you're building CommaFeed from sources with a JDK 17 or 21, you may need to update it to the most recent patch version to support `-proc:full` (#1618)
- fixed an issue that caused users without an email address set to be unable to edit their profile (#1184)
## [5.3.4]
## [4.0.0]
- Added support for Internationalized Domain Names (#1588)
- migrated from dropwizard 2 to dropwizard 4, Java 17+ is now required
- entries that were fetched and inserted in the database but not yet shown in the UI are no longer marked as read when ## [5.3.3]
marking all entries as read
- your custom sidebar width is now persisted in the local storage of your browser - Removed image bottom margins (#1587)
- there is now a third color scheme option in addition to light and dark: system (follows the system color scheme)
- added support for youtube playlist favicons ## [5.3.2]
- custom JS code is now executed when the app is done loading instead of when the page is loaded
- the favicon is now correctly returned for feeds that return an invalid content type - Fixed an issue that could cause some images from not being rendered correctly (#1587)
- the feed refresh engine now uses httpclient5 with connection pooling and no longer creates a new client for each
request, reducing CPU usage ## [5.3.1]
- updated UI library Mantine to 7.0, improving performance
- the h2 embedded database is now compacted on shutdown to reclaim unused space - Fixed an issue that could cause some HTTP feeds to return a 400 error (#1572)
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is
recommended (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0) ## [5.3.0]
- migrated documentation from swagger 2 to openapi 3
- added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser - Added a setting to set a cooldown on the "fetch all my feeds" action, disabled by default (#1556)
- the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be - Fixed an issue that could cause entries to not correctly load when using the "next" header button (#1557)
configured (see config.yml.example)
- the websocket connection now works correctly when the context root of the application is not "/" ## [5.2.0]
- unstable pubsubhubbub support was removed
- Added an option to keep a number of entries above the selected entry when scrolling
## [3.10.1] - Added a cache to the HTTP client to reduce the number of requests made to feeds when subscribing (#1431)
- Feeds are no longer refreshed between the moment its last user unsubscribes and the moment the feed is cleaned up (every hour)
- swap next and previous buttons (#1159) - Fixed an issue that could cause entries to not correctly load when using keyboard navigation (#1557)
- unread count for subscriptions will now be shortened starting at 10k instead of 1k
- increased websocket ping interval to just under a minute to reduce data and battery usage on mobile ## [5.1.1]
- only refresh subscription tree on a timer if websocket connection is unavailable
- the Docker image now uses less memory by returning unused memory to the OS - Fixed database migration issue when upgrading from 5.0.0 to 5.1.0 on MariaDB (#1544)
- add support for Java 21 - When feeds without unread entries are hidden from the tree, the feed is displayed in the tree until another one is selected (#1543)
## [3.10.0] ## [5.1.0]
- added a Fever-compatible API that is usable with mobile clients that support the Fever API (see instructions in - Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518)
Settings -> Profile) - Fixed an issue that could prevent the app from starting on some systems (#1532)
- long entry titles are no longer shortened in the detailed view - Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to date
- added the "s" keyboard shortcut to star/unstar entries - Reduced database cleanup log verbosity
- http sessions are now stored in the database (they were stored on disk before)
- fixed an issue that made it impossible to override the database url in a config.yml mounted in the Docker image ## [5.0.2]
## [3.9.0] - Fix favicon fetching for Youtube channels in native mode when Google auth key is set
- Fix an error that appears in the logs when fetching some favicons
- improve performance by disabling the loader when nothing is loading (most noticeable on mobile)
- added a setting to disable the 'mark all as read' confirmation ## [5.0.1]
- added a setting to disable the custom context menu
- if the custom context is enabled, it can still be disabled by pressing the shift key - Configure native compilation to support older CPU architectures (#1524)
- the announcement feature is now working again and supports html ('announcement' configuration element in config.yml)
- add support for MariaDB 11+ ## [5.0.0]
- fix entry header shortly rendered as mobile on desktop, causing a small visual glitch
- fix an issue that could cause a feed to not refresh correctly if the url was very long CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
- database cleanup batch size is now configurable the [announcement](https://github.com/Athou/commafeed/discussions/1517).
- css parsing errors are no longer logged to the standard output The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around
- fix small errors in the api documentation 0.3s) and very low memory footprint (< 50M).
## [3.8.1] - CommaFeed now has a different package for each supported database.
- If you are deploying CommaFeed with a precompiled package, please
- in expanded mode, don't scroll when clicking on the body of the current entry read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#download-a-precompiled-package).
- improve content cleanup task performance for instances with a very large number of feeds - If you are building CommaFeed from sources, please
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#build-from-sources).
## [3.8.0] - If you are using the Docker image, please read the instructions on
the [Docker Hub page](https://hub.docker.com/r/athou/commafeed).
- add previous and next buttons in the toolbar - Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone).
- add a setting to always scroll selected entry to the top of the page, even if it fits entirely on screen Please
- clicking on the body of an entry in expanded mode selects it and marks it as read read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration).
- add rich text editor with autocomplete for custom css and js code in settings (desktop only) Note that a lot of configuration elements have been removed or renamed and are now nested/grouped by feature.
- dramatically improve performance while scrolling - Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB.
- fix broken welcome page mobile layout - Use a different icon for filtering unread entries and marking an entry as read (#1506)
- format dates in user locale instead of GMT in relative date popups - Added various HTML attributes to ease custom JS/CSS customization (#1507)
- The Redis cache has been removed. There have been multiple enhancements to the feed refresh engine and it is no longer
## [3.7.0] needed, even for instances with a large number of feeds.
- The H2 migration tool that automatically upgrades H2 databases from format 2 to 3 has been removed. If you're using
- the sidebar is now resizable the H2 embedded database, please upgrade to at least version 4.3.0 before upgrading to CommaFeed 5.0.0.
- added the "f" keyboard shortcut to hide the sidebar
- added tooltips to relative dates with the exact date ## [4.6.0]
- add a setting to hide commafeed from search engines (exposes a robots.txt file, enabled by default)
- the browser extension unread count now updates when articles are marked as read/unread in the app - switched from Temurin to OpenJ9 as the JVM used in the Docker image, resulting in memory usage reduction by up to 50%
- The "b" keyboard shortcut now works as expected on Chrome but requires the browser extension to be installed - fix an issue that could cause old entries to reappear if they were updated by their author (#1486)
- dark mode has been disabled on the api documentation page as it was unreadable - show all entries regardless of their read status when searching with keywords, even if the ui is configured to show
- improvement to the feed refresh queuing logic when "heavy load" mode is enabled unread entries only
- fix a bug that could prevent feeds and categories from being edited
## [4.5.0]
## [3.6.0]
- significantly reduce the time needed to retrieve entries or mark them as read, especially when there are a lot of
- add a button to open CommaFeed in a new tab and a button to open options when using the browser extension entries (#1452)
- clicking on the entry title in expanded mode now opens the link instead of doing nothing - fix a race condition where a feed could be refreshed before it was created in the database
- add tooltips to buttons when the mobile layout is used on desktop - fix an issue that could cause the websocket notification to contain the wrong number of unread entries when using
- redirect the user to the welcome page if the user was deleted from the database mysql/mariadb
- add link to api documentation on welcome page - fix an error when trying to mark all starred entries as read
- the unread count is now correctly updated when using the "/next" bookmarklet while redis cache is enabled - remove the `onlyIds` parameter from REST endpoints since retrieving all the entries is now just as fast
- remove support for microsoft sqlserver because it's not covered with integration tests (please open an issue if you'd
## [3.5.0] like it back)
- add compatibility with the new version of the CommaFeed browser extension ## [4.4.1]
- disable pull-to-refresh on mobile as it messes with vertical scrolling
- add css classes to feed entries to help with custom css rules - fix vertical scrolling issues with Safari (#1168)
- api documentation page no longer requires users to be authenticated - the default value for new users for the "star entry" button and the "open in new tab" button in the entry headers is
- add a setting to limit the number of feeds a user can subscribe to now "on desktop" instead of "always"
- add a setting to disable strict password policy - the "keyboard shortcuts" help page now shows "Cmd" instead of "Ctrl" on macOS (#1389)
- add feed refresh engine metrics - remove a superfluous feed fetch when subscribing to a feed (#1431)
- fix redis timeouts - the Docker image now uses Java 21
## [3.4.0] ## [4.4.0]
- add support for arm64 docker images - add support for sharing using the browser native capabilities if available (#1255)
- add divider to visually separate read-only information from form on the profile settings page - add a button in the entry headers to star an entry (#1025)
- reduce javascript bundle size by 30% by loading only the necessary translations - add a button in the entry headers to open links in a new tab (#1333)
- add a standalone donate page with all ways to support CommaFeed - add two options in the settings to toggle those buttons
- fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots - accept .opml file extension when importing and export with the .opml extension
of feeds - the "mark as read" option is no longer shown in the context menu for entries that are too old to be marked as read (
- fix alignment of icon with text for category tree nodes older than `keepStatusDays`) (#1303)
- fix alignment of burger button with the rest of the header on mobile
## [4.3.3]
## [3.3.2]
- fix OPML import (#1279)
- restore entry selection indicator (left orange border) that was lost with the mantine 6.x upgrade (3.3.0)
- add dividers to visually separate read-only information from forms on feed and category details pages ## [4.3.2]
- reduced javascript bundle size by 10%
- added support for unix sockets (#1278)
## [3.3.1]
## [4.3.1]
- fix long feed names not being shortened to respect tree max width
- fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database and the database
## [3.3.0] timezone is not UTC (#1239)
- videos in enclosures can no longer have a width larger than the page (#1240)
- there are now database changes, rolling back to 2.x will no longer be possible
- restore support for user custom CSS rules ## [4.3.0]
- add support for user custom JS code that will be executed on page load
- h2 (the embedded database) has been upgraded to 2.2.224
## [3.2.0] - this version uses a different file format than 2.1.x, the first time you start CommaFeed with this version, the
database will be automatically converted to the new format
- restore the welcome page - add a setting to completely disable scrolling to selected entry (#1157)
- only apply hover effect for unread entries (same as commafeed v2) - add a css class reflecting the current view mode to ease custom css rules (#1232)
- move notifications at the bottom of the screen - fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database (#1239)
- always use https for sharing urls
- add support for redis ACLs ## [4.2.1]
- transition to google analytics v4
- fix an issue that caused the tree to show an incorrect unread count after a websocket notification because entries
## [3.1.0] that were already marked as read by a filtering expression were not ignored (#1191)
- add an even more compact layout ## [4.2.0]
- restore hover effect from commafeed 2.x
- view mode (compact, expanded, ...) is now stored on the device so you can have a different view mode on desktop and - add a setting to display the action buttons in the footer instead of in the header on mobile (#1121)
mobile - the websocket notification now contains everything needed to update the UI, the client no longer needs to make an API
- fix for the "Illegal attempt to associate a collection with two open sessions." error call to get the latest data when receiving the notification
- feed fetching workflow is now orchestrated with rxjava, removing a lot of code - add a workaround to the Fever API for the Unread iOS app (#1188)
- fix an issue that caused dates to be saved incorrectly if the database server and the application server were in
## [3.0.1] different timezones (#1187)
- allow env variable substitution in config.yml ## [4.1.0]
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with its
value - it is now possible to open the sidebar on mobile by swiping to the right (#1098)
- allow env variable prefixed with `CF_` to override config.yml properties - swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true` - the full hierarchy of categories are now displayed in the category dropdown (#1045)
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup.
## [3.0.0] The setting is disabled by default for existing installations, except for the docker image where it is enabled and set
to 365 days
- complete overhaul of the UI - if user registrations are disabled on your instance which is the default behavior, users are redirected on the login
- backend and frontend are now in separate maven modules page instead of the welcome page when not logged in (#1185)
- no changes to the api or the database - the sidebar resizer is no longer shown in the middle of the screen on mobile
- Docker images are now automatically built and available at https://hub.docker.com/r/athou/commafeed - when using the system color scheme and the system is using a dark theme, feed entries no longer flicker on load
- the demo account (if enabled) cannot register custom javascript code anymore
## [2.6.0] - removed the usage of `toSorted` in the client because older browsers do not support it (#1183)
- the openapi documentation is no longer cached by the browser so you always have access to the latest version
- add support for media content as a backup for missing content (useful for youtube feeds) - added a memory management section to the readme, reading it is recommended if you are running CommaFeed on a server
- correctly follow http error code 308 redirects with limited memory
- fixed a bug that prevented users from deleting their account - fixed an issue that caused users without an email address set to be unable to edit their profile (#1184)
- fixed a bug that made commafeed store entry contents multiple times
- fixed a bug that prevented the app to be used as an installed app on mobile devices if the context path of commafeed ## [4.0.0]
was not "/"
- fixed a bug that prevented entries from being "marked as read older than xxx" for a feed that was just added - migrated from dropwizard 2 to dropwizard 4, Java 17+ is now required
- removed support for google+ and readability as those services no longer exist - entries that were fetched and inserted in the database but not yet shown in the UI are no longer marked as read when
- removed support for deploying on openshift marking all entries as read
- removed alphabetical sorting of entries because of really poor performance (title cannot be indexed) - your custom sidebar width is now persisted in the local storage of your browser
- improve performance for instances with the heavy load setting enabled by preventing CommaFeed from fetching feeds from - there is now a third color scheme option in addition to light and dark: system (follows the system color scheme)
users that did not log in for a long time - added support for youtube playlist favicons
- various dependencies upgrades (notably dropwizard from 1.3 to 2.1) - custom JS code is now executed when the app is done loading instead of when the page is loaded
- add support for mariadb - the favicon is now correctly returned for feeds that return an invalid content type
- add support for java17+ runtime - the feed refresh engine now uses httpclient5 with connection pooling and no longer creates a new client for each
- various security improvements request, reducing CPU usage
- updated UI library Mantine to 7.0, improving performance
## [2.5.0] - the h2 embedded database is now compacted on shutdown to reclaim unused space
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is
- unread count is now displayed in a favicon badge when supported recommended (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
- the user agent string for the bot fetching feeds is now configurable - migrated documentation from swagger 2 to openapi 3
- feed parsing performance improvements - added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser
- support for java9+ runtime - the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be
- can now properly start from an empty postgresql database configured (see config.yml.example)
- the websocket connection now works correctly when the context root of the application is not "/"
## [2.4.0] - unstable pubsubhubbub support was removed
- users were not able to change password or delete account ## [3.10.1]
- fix api key generation
- feed entries can now be sorted alphabetically - swap next and previous buttons (#1159)
- fix facebook sharing - unread count for subscriptions will now be shortened starting at 10k instead of 1k
- fix layout on iOS - increased websocket ping interval to just under a minute to reduce data and battery usage on mobile
- postgresql driver update (fix for postgres 9.6) - only refresh subscription tree on a timer if websocket connection is unavailable
- various internationalization fixes - the Docker image now uses less memory by returning unused memory to the OS
- security fixes - add support for Java 21
## [2.3.0] ## [3.10.0]
- dropwizard upgrade 0.9.1 - added a Fever-compatible API that is usable with mobile clients that support the Fever API (see instructions in
- feed enclosures are hidden if they already displayed in the content Settings -> Profile)
- fix youtube favicons - long entry titles are no longer shortened in the detailed view
- various internationalization fixes - added the "s" keyboard shortcut to star/unstar entries
- http sessions are now stored in the database (they were stored on disk before)
## [2.2.0] - fixed an issue that made it impossible to override the database url in a config.yml mounted in the Docker image
- fix youtube and instagram favicon fetching ## [3.9.0]
- mark as read filter was lost when a feed was rearranged with drag&drop
- feed entry categories are now displayed if available - improve performance by disabling the loader when nothing is loading (most noticeable on mobile)
- various performance and dependencies upgrades - added a setting to disable the 'mark all as read' confirmation
- java8 is now required - added a setting to disable the custom context menu
- if the custom context is enabled, it can still be disabled by pressing the shift key
## [2.1.0] - the announcement feature is now working again and supports html ('announcement' configuration element in config.yml)
- add support for MariaDB 11+
- dropwizard upgrade to 0.8.0 - fix entry header shortly rendered as mobile on desktop, causing a small visual glitch
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use - fix an issue that could cause a feed to not refresh correctly if the url was very long
server.applicationContextPath instead - database cleanup batch size is now configurable
- new setting app.maxFeedCapacity for deleting old entries - css parsing errors are no longer logged to the standard output
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title, - fix small errors in the api documentation
content, author or url.
- ability to use !keyword or -keyword to exclude a keyword from a search query ## [3.8.1]
- facebook feeds now show user favicon instead of facebook favicon
- new dark theme 'nightsky' - in expanded mode, don't scroll when clicking on the body of the current entry
- improve content cleanup task performance for instances with a very large number of feeds
## [2.0.3]
## [3.8.0]
- internet explorer ajax cache workaround
- categories are now deletable again - add previous and next buttons in the toolbar
- openshift support is back - add a setting to always scroll selected entry to the top of the page, even if it fits entirely on screen
- youtube feeds now show user favicon instead of youtube favicon - clicking on the body of an entry in expanded mode selects it and marks it as read
- add rich text editor with autocomplete for custom css and js code in settings (desktop only)
## [2.0.2] - dramatically improve performance while scrolling
- fix broken welcome page mobile layout
- api using the api key is now working again - format dates in user locale instead of GMT in relative date popups
- context path is now configurable in config.yml (see app.contextPath in config.yml.example)
- fix login on firefox when fields are autofilled by the browser ## [3.7.0]
- fix scrolling of subscriptions list on mobile
- user is now logged in after registration - the sidebar is now resizable
- fix link to documentation on home page and about page - added the "f" keyboard shortcut to hide the sidebar
- fields autocomplete is disabled on the profile page - added tooltips to relative dates with the exact date
- users are able to delete their account again - add a setting to hide commafeed from search engines (exposes a robots.txt file, enabled by default)
- chinese and malaysian translation files are now correctly loaded - the browser extension unread count now updates when articles are marked as read/unread in the app
- software version in user-agent when fetching feeds is no longer hardcoded - The "b" keyboard shortcut now works as expected on Chrome but requires the browser extension to be installed
- admin settings page is now read only, settings are configured in config.yml - dark mode has been disabled on the api documentation page as it was unreadable
- added link to metrics on the admin settings page - improvement to the feed refresh queuing logic when "heavy load" mode is enabled
- Rome (rss library) upgrade to 1.5.0 - fix a bug that could prevent feeds and categories from being edited
## [2.0.1] ## [3.6.0]
- the redis pool no longer throws an exception when it is unable to aquire a new connection - add a button to open CommaFeed in a new tab and a button to open options when using the browser extension
- clicking on the entry title in expanded mode now opens the link instead of doing nothing
## [2.0.0] - add tooltips to buttons when the mobile layout is used on desktop
- redirect the user to the welcome page if the user was deleted from the database
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory - add link to api documentation on welcome page
consumption and better overall performances. - the unread count is now correctly updated when using the "/next" bookmarklet while redis cache is enabled
See the README on how to build CommaFeed from now on.
- CommaFeed should no longer fetch the same feed multiple times in a row ## [3.5.0]
- Users can use their username or email to log in
- add compatibility with the new version of the CommaFeed browser extension
- disable pull-to-refresh on mobile as it messes with vertical scrolling
- add css classes to feed entries to help with custom css rules
- api documentation page no longer requires users to be authenticated
- add a setting to limit the number of feeds a user can subscribe to
- add a setting to disable strict password policy
- add feed refresh engine metrics
- fix redis timeouts
## [3.4.0]
- add support for arm64 docker images
- add divider to visually separate read-only information from form on the profile settings page
- reduce javascript bundle size by 30% by loading only the necessary translations
- add a standalone donate page with all ways to support CommaFeed
- fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots
of feeds
- fix alignment of icon with text for category tree nodes
- fix alignment of burger button with the rest of the header on mobile
## [3.3.2]
- restore entry selection indicator (left orange border) that was lost with the mantine 6.x upgrade (3.3.0)
- add dividers to visually separate read-only information from forms on feed and category details pages
- reduced javascript bundle size by 10%
## [3.3.1]
- fix long feed names not being shortened to respect tree max width
## [3.3.0]
- there are now database changes, rolling back to 2.x will no longer be possible
- restore support for user custom CSS rules
- add support for user custom JS code that will be executed on page load
## [3.2.0]
- restore the welcome page
- only apply hover effect for unread entries (same as commafeed v2)
- move notifications at the bottom of the screen
- always use https for sharing urls
- add support for redis ACLs
- transition to google analytics v4
## [3.1.0]
- add an even more compact layout
- restore hover effect from commafeed 2.x
- view mode (compact, expanded, ...) is now stored on the device so you can have a different view mode on desktop and
mobile
- fix for the "Illegal attempt to associate a collection with two open sessions." error
- feed fetching workflow is now orchestrated with rxjava, removing a lot of code
## [3.0.1]
- allow env variable substitution in config.yml
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with its
value
- allow env variable prefixed with `CF_` to override config.yml properties
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
## [3.0.0]
- complete overhaul of the UI
- backend and frontend are now in separate maven modules
- no changes to the api or the database
- Docker images are now automatically built and available at https://hub.docker.com/r/athou/commafeed
## [2.6.0]
- add support for media content as a backup for missing content (useful for youtube feeds)
- correctly follow http error code 308 redirects
- fixed a bug that prevented users from deleting their account
- fixed a bug that made commafeed store entry contents multiple times
- fixed a bug that prevented the app to be used as an installed app on mobile devices if the context path of commafeed
was not "/"
- fixed a bug that prevented entries from being "marked as read older than xxx" for a feed that was just added
- removed support for google+ and readability as those services no longer exist
- removed support for deploying on openshift
- removed alphabetical sorting of entries because of really poor performance (title cannot be indexed)
- improve performance for instances with the heavy load setting enabled by preventing CommaFeed from fetching feeds from
users that did not log in for a long time
- various dependencies upgrades (notably dropwizard from 1.3 to 2.1)
- add support for mariadb
- add support for java17+ runtime
- various security improvements
## [2.5.0]
- unread count is now displayed in a favicon badge when supported
- the user agent string for the bot fetching feeds is now configurable
- feed parsing performance improvements
- support for java9+ runtime
- can now properly start from an empty postgresql database
## [2.4.0]
- users were not able to change password or delete account
- fix api key generation
- feed entries can now be sorted alphabetically
- fix facebook sharing
- fix layout on iOS
- postgresql driver update (fix for postgres 9.6)
- various internationalization fixes
- security fixes
## [2.3.0]
- dropwizard upgrade 0.9.1
- feed enclosures are hidden if they already displayed in the content
- fix youtube favicons
- various internationalization fixes
## [2.2.0]
- fix youtube and instagram favicon fetching
- mark as read filter was lost when a feed was rearranged with drag&drop
- feed entry categories are now displayed if available
- various performance and dependencies upgrades
- java8 is now required
## [2.1.0]
- dropwizard upgrade to 0.8.0
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use
server.applicationContextPath instead
- new setting app.maxFeedCapacity for deleting old entries
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title,
content, author or url.
- ability to use !keyword or -keyword to exclude a keyword from a search query
- facebook feeds now show user favicon instead of facebook favicon
- new dark theme 'nightsky'
## [2.0.3]
- internet explorer ajax cache workaround
- categories are now deletable again
- openshift support is back
- youtube feeds now show user favicon instead of youtube favicon
## [2.0.2]
- api using the api key is now working again
- context path is now configurable in config.yml (see app.contextPath in config.yml.example)
- fix login on firefox when fields are autofilled by the browser
- fix scrolling of subscriptions list on mobile
- user is now logged in after registration
- fix link to documentation on home page and about page
- fields autocomplete is disabled on the profile page
- users are able to delete their account again
- chinese and malaysian translation files are now correctly loaded
- software version in user-agent when fetching feeds is no longer hardcoded
- admin settings page is now read only, settings are configured in config.yml
- added link to metrics on the admin settings page
- Rome (rss library) upgrade to 1.5.0
## [2.0.1]
- the redis pool no longer throws an exception when it is unable to aquire a new connection
## [2.0.0]
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
consumption and better overall performances.
See the README on how to build CommaFeed from now on.
- CommaFeed should no longer fetch the same feed multiple times in a row
- Users can use their username or email to log in

View File

@@ -1,12 +0,0 @@
FROM eclipse-temurin:17-jre
EXPOSE 8082
RUN mkdir -p /commafeed/data
VOLUME /commafeed/data
COPY commafeed-server/config.yml.example config.yml
COPY commafeed-server/target/commafeed.jar .
ENV JAVA_TOOL_OPTIONS -Djava.net.preferIPv4Stack=true -Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
CMD ["java", "-jar", "commafeed.jar", "server", "config.yml"]

60
LICENSE
View File

@@ -1,31 +1,31 @@
Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions. 1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. 2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. 3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. 4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. 5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. 6. Trademarks.
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. 7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. 8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. 9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS

123
README.md
View File

@@ -1,6 +1,6 @@
# CommaFeed # CommaFeed
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript. Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeScript.
![preview](https://user-images.githubusercontent.com/1256795/184886828-1973f148-58a9-4c6d-9587-ee5e5d3cc2cb.png) ![preview](https://user-images.githubusercontent.com/1256795/184886828-1973f148-58a9-4c6d-9587-ee5e5d3cc2cb.png)
@@ -8,14 +8,23 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
- 4 different layouts - 4 different layouts
- Light/Dark theme - Light/Dark theme
- Fully responsive - Fully responsive, works great on both mobile and desktop
- Keyboard shortcuts for almost everything - Keyboard shortcuts for almost everything
- Support for right-to-left feeds - Support for right-to-left feeds
- Translated in 25+ languages - Translated in 25+ languages
- Supports thousands of users and millions of feeds - Supports thousands of users and millions of feeds
- OPML import/export - OPML import/export
- REST API and a Fever-compatible API for native mobile apps - REST API
- Fever-compatible API for native mobile apps
- Can automatically mark articles as read based on user-defined rules
- 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
- Supports 4 databases
- H2 (embedded database)
- PostgreSQL
- MySQL
- MariaDB
## Deployment ## Deployment
@@ -33,50 +42,116 @@ PikaPods shares 20% of the revenue back to CommaFeed.
[![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=commafeed) [![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=commafeed)
### Download precompiled package ### Download a precompiled package
mkdir commafeed && cd commafeed Go to the [release page](https://github.com/Athou/commafeed/releases) and download the latest version for your operating
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar system and database of choice.
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
The server will listen on http://localhost:8082. The default There are two types of packages:
user is `admin` and the default password is `admin`.
- The `linux-x86_64`, `linux-aarch_64` and `windows-x86_64` packages are compiled natively and contain an executable that can be run
directly.
- The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all
platforms but requires a JRE and is started with `java -jar quarkus-run.jar`.
If available for your operating system, the native package is recommended because it has a faster startup time and lower
memory usage.
### Build from sources ### Build from sources
git clone https://github.com/Athou/commafeed.git ./mvnw clean package [-P<database> [-Pnative]] [-DskipTests]
cd commafeed
./mvnw clean package
cp commafeed-server/config.yml.example config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
The server will listen on http://localhost:8082. The default - `<database>` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. The default is `h2`.
user is `admin` and the default password is `admin`. - `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment
variable pointing to a GraalVM installation).
- `-DskipTests` to speed up the build process by skipping tests.
When the build is complete:
- a zip containing all jars required to run the application is located at
`commafeed-server/target/commafeed-<version>-<database>-jvm.zip`. Extract it and run the application with
`java -jar quarkus-run.jar`
- if you used the native profile, the executable is located at
`commafeed-server/target/commafeed-<version>-<database>-<platform>-<arch>-runner[.exe]`
### Distribution packages
- Arch Linux users can use [the CommaFeed package on AUR](https://aur.archlinux.org/pkgbase/commafeed), which builds native binaries with GraalVM for all supported databases.
## Configuration
CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in
the `data` directory of the current directory.
To use a different database, you will need to configure the following properties:
- `quarkus.datasource.jdbc.url`
- e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE`
- e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed`
- e.g. for MySQL:
`jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
- e.g. for MariaDB:
`jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
- `quarkus.datasource.username`
- `quarkus.datasource.password`
There are multiple ways to configure CommaFeed:
- a `config/application.properties` [properties](https://en.wikipedia.org/wiki/.properties) file relative to the working
directory (keys in kebab-case)
- Command line arguments each prefixed with `-D` (keys in kebab-case)
- Environment variables (keys in UPPER_CASE)
- a `.env` file in the working directory (keys in UPPER_CASE)
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
All [CommaFeed settings](https://athou.github.io/commafeed/documentation) are optional and have sensible default values.
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters).
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
When started, the server will listen on http://localhost:8082.
The default user is `admin` and the default password is `admin`.
### Updates
When CommaFeed is up and running, you can subscribe to [this feed](https://github.com/Athou/commafeed/releases.atom) to be notified of new releases.
### Memory management ### Memory management
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
operating system. This is because acquiring memory from the operating system is a relatively expensive operation. operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
However, this can be problematic on systems with limited memory. This can be problematic on systems with limited memory.
#### Hard limit #### Hard limit (`native` and `jvm` packages)
The JVM can be configured to use a maximum amount of memory with the `-Xmx` parameter. The JVM can be configured to use a maximum amount of memory with the `-Xmx` parameter.
For example, to limit the JVM to 256MB of memory, use `-Xmx256m`. For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
#### Dynamic sizing #### Dynamic sizing (`jvm` package)
The JVM can be configured to release unused memory to the operating system with the following parameters: In addition to the previous setting, the JVM can be configured to release unused memory to the operating system with the
following parameters:
-Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10 -Xms20m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
This is how the Docker image is configured.
See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html) See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for
more more
information. information.
#### OpenJ9 (`jvm` package)
The [OpenJ9](https://eclipse.dev/openj9/) JVM is a more memory-efficient alternative to the HotSpot JVM, at the cost of
slightly slower throughput.
IBM provides precompiled binaries for OpenJ9
named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/).
This is the JVM used in
the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm).
## Translation ## Translation
Files for internationalization are Files for internationalization are
@@ -99,7 +174,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
- Open `commafeed-server` in your preferred Java IDE. - Open `commafeed-server` in your preferred Java IDE.
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE. - CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments - run `./mvnw quarkus:dev`
### Frontend ### Frontend

View File

@@ -1,8 +0,0 @@
dist
node_modules
vite.config.ts
# compiled linguijs locales
# they no longer exist but we keep this to avoid issues with people still having those files on disk
src/locales/**/*.ts

View File

@@ -1,52 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"standard",
"love",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended",
],
settings: {
react: {
version: "detect",
},
},
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parserOptions: {
project: true,
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["react"],
rules: {
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-confusing-void-expression": ["error", { ignoreArrowShorthand: true }],
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/prefer-nullish-coalescing": ["error", { ignoreConditionalTests: true }],
"@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }],
"@typescript-eslint/strict-boolean-expressions": "off",
"react/jsx-curly-brace-presence": ["error", "never"],
"react/no-unescaped-entities": "off",
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "error",
},
}

View File

@@ -1,52 +0,0 @@
{
"locales": [
"ar",
"ca",
"cs",
"cy",
"da",
"de",
"en",
"es",
"fa",
"fi",
"fr",
"gl",
"hu",
"id",
"it",
"ja",
"ko",
"ms",
"nb",
"nl",
"nn",
"pl",
"pt",
"ru",
"sk",
"sv",
"tr",
"zh"
],
"catalogs": [
{
"path": "src/locales/{locale}/messages",
"include": [
"src"
],
"exclude": [
"src/locales/**"
]
}
],
"format": "po",
"formatOptions": {
"origins": true,
"lineNumbers": false
},
"sourceLocale": "en",
"fallbackLocales": {
"default": "en"
}
}

View File

@@ -1,8 +0,0 @@
{
"printWidth": 140,
"semi": false,
"tabWidth": 4,
"arrowParens": "avoid",
"endOfLine": "auto",
"trailingComma": "es5"
}

View File

@@ -0,0 +1,27 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"formatter": {
"indentStyle": "space",
"indentWidth": 4,
"lineEnding": "lf",
"lineWidth": 140
},
"javascript": {
"formatter": {
"trailingCommas": "es5",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded"
}
},
"linter": {
"rules": {
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error"
}
}
},
"files": {
"ignore": ["dist", "node_modules", "target", "target-ide"]
}
}

View File

@@ -1,11 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>CommaFeed</title> <meta name="referrer" content="no-referrer" />
<title>CommaFeed</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -0,0 +1,52 @@
import type { LinguiConfig } from "@lingui/conf"
const config: LinguiConfig = {
locales: [
"ar",
"ca",
"cs",
"cy",
"da",
"de",
"en",
"es",
"fa",
"fi",
"fr",
"gl",
"hu",
"id",
"it",
"ja",
"ko",
"ms",
"nb",
"nl",
"nn",
"pl",
"pt",
"ru",
"sk",
"sv",
"tr",
"zh",
],
catalogs: [
{
path: "src/locales/{locale}/messages",
include: ["src"],
exclude: ["src/locales/**"],
},
],
format: "po",
formatOptions: {
origins: true,
lineNumbers: false,
},
sourceLocale: "en",
fallbackLocales: {
default: "en",
},
}
export default config

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +1,85 @@
{ {
"name": "commafeed-client", "name": "commafeed-client",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite",
"dev:typescript": "tsc --watch", "dev:host": "vite --host",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"test:ci": "vitest run", "test:ci": "vitest run",
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src", "lint": "biome check",
"i18n:extract": "lingui extract --clean" "lint:fix": "biome check --write",
}, "i18n:extract": "lingui extract --clean"
"dependencies": { },
"@emotion/react": "^11.11.4", "dependencies": {
"@fontsource/open-sans": "^5.0.27", "@emotion/react": "^11.14.0",
"@lingui/core": "^4.10.0", "@fontsource/open-sans": "^5.2.5",
"@lingui/macro": "^4.10.0", "@lingui/core": "^5.3.1",
"@lingui/react": "^4.10.0", "@lingui/react": "^5.3.1",
"@mantine/core": "^7.8.0", "@mantine/core": "^8.0.2",
"@mantine/form": "^7.8.0", "@mantine/form": "^8.0.2",
"@mantine/hooks": "^7.8.0", "@mantine/hooks": "^8.0.2",
"@mantine/modals": "^7.8.0", "@mantine/modals": "^8.0.2",
"@mantine/notifications": "^7.8.0", "@mantine/notifications": "^8.0.2",
"@mantine/spotlight": "^7.8.0", "@mantine/spotlight": "^8.0.2",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.7.0",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "^2.8.2",
"axios": "^1.6.8", "axios": "^1.9.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.13",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.1",
"monaco-editor": "^0.47.0", "monaco-editor": "^0.52.2",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"react": "^18.2.0", "react": "^19.1.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",
"react-dom": "^18.2.0", "react-device-detect": "^2.2.3",
"react-draggable": "^4.4.6", "react-dom": "^19.1.0",
"react-ga4": "^2.1.0", "react-draggable": "^4.4.6",
"react-icons": "^5.0.1", "react-ga4": "^2.1.0",
"react-infinite-scroller": "^1.2.6", "react-icons": "^5.5.0",
"react-redux": "^9.1.0", "react-infinite-scroller": "^1.2.6",
"react-router-dom": "^6.22.3", "react-redux": "^9.2.0",
"react-swipeable": "^7.0.1", "react-router-dom": "^7.6.1",
"redoc": "^2.1.3", "react-swipeable": "^7.0.2",
"throttle-debounce": "^5.0.0", "redoc": "^2.5.0",
"tinycon": "^0.6.8", "style-to-object": "^1.0.8",
"tss-react": "^4.9.6", "throttle-debounce": "^5.0.2",
"use-local-storage": "^3.0.0", "tinycon": "^0.6.8",
"websocket-heartbeat-js": "^1.1.3" "tss-react": "^4.9.18",
}, "websocket-heartbeat-js": "^1.1.3"
"devDependencies": { },
"@lingui/cli": "^4.10.0", "devDependencies": {
"@lingui/vite-plugin": "^4.10.0", "@biomejs/biome": "^1.9.4",
"@types/mousetrap": "^1.6.15", "@lingui/babel-plugin-lingui-macro": "^5.3.1",
"@types/react": "^18.2.78", "@lingui/cli": "^5.3.1",
"@types/react-dom": "^18.2.25", "@lingui/vite-plugin": "^5.3.1",
"@types/react-infinite-scroller": "^1.2.5", "@testing-library/jest-dom": "^6.6.3",
"@types/swagger-ui-react": "^4.18.3", "@testing-library/react": "^16.3.0",
"@types/throttle-debounce": "^5.0.2", "@testing-library/user-event": "^14.6.1",
"@types/tinycon": "^0.6.5", "@types/mousetrap": "^1.6.15",
"@typescript-eslint/eslint-plugin": "^7.6.0", "@types/react": "^19.1.6",
"@vitejs/plugin-react": "^4.2.1", "@types/react-dom": "^19.1.5",
"babel-plugin-macros": "^3.1.0", "@types/react-infinite-scroller": "^1.2.5",
"eslint": "^8.57.0", "@types/throttle-debounce": "^5.0.2",
"eslint-config-love": "^47.0.0", "@types/tinycon": "^0.6.7",
"eslint-config-prettier": "^9.1.0", "@vitejs/plugin-react": "^4.5.0",
"eslint-config-standard": "^17.1.0", "babel-plugin-macros": "^3.1.0",
"eslint-plugin-prettier": "^5.1.3", "babel-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react": "^7.34.1", "jsdom": "^26.1.0",
"eslint-plugin-react-hooks": "^4.6.0", "rollup-plugin-visualizer": "^6.0.0",
"prettier": "^3.2.5", "typescript": "^5.8.3",
"rollup-plugin-visualizer": "^5.12.0", "vite": "^6.3.5",
"typescript": "^5.4.5", "vite-plugin-checker": "^0.9.3",
"vite": "^5.2.8", "vite-tsconfig-paths": "^5.1.4",
"vite-plugin-eslint": "^1.8.1", "vitest": "^3.1.4"
"vite-tsconfig-paths": "^4.3.2", },
"vitest": "^1.5.0", "overrides": {
"vitest-mock-extended": "^1.3.1" "react-infinite-scroller": {
} "react": "^19.1.0"
}
}
} }

View File

@@ -1,88 +1,97 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.commafeed</groupId> <parent>
<artifactId>commafeed</artifactId> <groupId>com.commafeed</groupId>
<version>4.4.0</version> <artifactId>commafeed</artifactId>
</parent> <version>5.10.0</version>
<artifactId>commafeed-client</artifactId> </parent>
<name>CommaFeed Client</name> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
<build>
<plugins> <properties>
<plugin> <!-- renovate: datasource=node-version depName=node -->
<groupId>com.github.eirslett</groupId> <node.version>v22.16.0</node.version>
<artifactId>frontend-maven-plugin</artifactId> <!-- renovate: datasource=npm depName=npm -->
<version>1.15.0</version> <npm.version>11.4.1</npm.version>
<?m2e ignore?> </properties>
<executions>
<execution> <build>
<id>install node and npm</id> <plugins>
<goals> <plugin>
<goal>install-node-and-npm</goal> <groupId>com.github.eirslett</groupId>
</goals> <artifactId>frontend-maven-plugin</artifactId>
<phase>compile</phase> <version>1.15.1</version>
<configuration> <?m2e ignore?>
<nodeVersion>v20.10.0</nodeVersion> <executions>
<npmVersion>10.2.5</npmVersion> <execution>
</configuration> <id>install node and npm</id>
</execution> <goals>
<execution> <goal>install-node-and-npm</goal>
<id>npm install</id> </goals>
<goals> <phase>compile</phase>
<goal>npm</goal> <configuration>
</goals> <nodeVersion>${node.version}</nodeVersion>
<phase>compile</phase> <npmVersion>${npm.version}</npmVersion>
<configuration> </configuration>
<arguments>ci</arguments> </execution>
</configuration> <execution>
</execution> <id>npm install</id>
<execution> <goals>
<id>npm run test</id> <goal>npm</goal>
<goals> </goals>
<goal>npm</goal> <phase>compile</phase>
</goals> <configuration>
<phase>compile</phase> <arguments>ci</arguments>
<configuration> </configuration>
<arguments>run test:ci</arguments> </execution>
</configuration> <execution>
</execution> <id>npm run test</id>
<execution> <goals>
<id>npm run build</id> <goal>npm</goal>
<goals> </goals>
<goal>npm</goal> <phase>compile</phase>
</goals> <configuration>
<phase>compile</phase> <arguments>run test:ci</arguments>
<configuration> <skip>${skipTests}</skip>
<arguments>run build</arguments> </configuration>
</configuration> </execution>
</execution> <execution>
</executions> <id>npm run build</id>
</plugin> <goals>
<plugin> <goal>npm</goal>
<artifactId>maven-resources-plugin</artifactId> </goals>
<version>3.3.1</version> <phase>compile</phase>
<executions> <configuration>
<execution> <arguments>run build</arguments>
<id>copy web interface to resources</id> </configuration>
<phase>prepare-package</phase> </execution>
<goals> </executions>
<goal>copy-resources</goal> </plugin>
</goals> <plugin>
<configuration> <artifactId>maven-resources-plugin</artifactId>
<outputDirectory>${project.build.directory}/classes/assets</outputDirectory> <version>3.3.1</version>
<resources> <executions>
<resource> <execution>
<directory>dist</directory> <id>copy web interface to resources</id>
<filtering>false</filtering> <phase>prepare-package</phase>
</resource> <goals>
</resources> <goal>copy-resources</goal>
</configuration> </goals>
</execution> <configuration>
</executions> <outputDirectory>${project.build.directory}/classes/META-INF/resources</outputDirectory>
</plugin> <resources>
</plugins> <resource>
</build> <directory>dist</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -1,7 +1,6 @@
import { i18n } from "@lingui/core" import { i18n } from "@lingui/core"
import { I18nProvider } from "@lingui/react" import { I18nProvider } from "@lingui/react"
import { MantineProvider } from "@mantine/core" import { MantineProvider } from "@mantine/core"
import { useDidUpdate } from "@mantine/hooks"
import { ModalsProvider } from "@mantine/modals" import { ModalsProvider } from "@mantine/modals"
import { Notifications } from "@mantine/notifications" import { Notifications } from "@mantine/notifications"
import { Constants } from "app/constants" import { Constants } from "app/constants"
@@ -9,11 +8,14 @@ import { redirectTo } from "app/redirect/slice"
import { reloadServerInfos } from "app/server/thunks" import { reloadServerInfos } from "app/server/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { categoryUnreadCount } from "app/utils" import { categoryUnreadCount } from "app/utils"
import { DisablePullToRefresh } from "components/DisablePullToRefresh"
import { ErrorBoundary } from "components/ErrorBoundary" import { ErrorBoundary } from "components/ErrorBoundary"
import { Header } from "components/header/Header" import { Header } from "components/header/Header"
import { Tree } from "components/sidebar/Tree" import { Tree } from "components/sidebar/Tree"
import { useAppLoading } from "hooks/useAppLoading"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useI18n } from "i18n" import { useI18n } from "i18n"
import { WelcomePage } from "pages/WelcomePage"
import { AdminUsersPage } from "pages/admin/AdminUsersPage" import { AdminUsersPage } from "pages/admin/AdminUsersPage"
import { MetricsPage } from "pages/admin/MetricsPage" import { MetricsPage } from "pages/admin/MetricsPage"
import { AboutPage } from "pages/app/AboutPage" import { AboutPage } from "pages/app/AboutPage"
@@ -28,19 +30,20 @@ import { TagDetailsPage } from "pages/app/TagDetailsPage"
import { LoginPage } from "pages/auth/LoginPage" import { LoginPage } from "pages/auth/LoginPage"
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage" import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
import { RegistrationPage } from "pages/auth/RegistrationPage" import { RegistrationPage } from "pages/auth/RegistrationPage"
import { WelcomePage } from "pages/WelcomePage" import React, { useEffect, useState } from "react"
import React, { useEffect, useRef } from "react" import { isSafari } from "react-device-detect"
import ReactGA from "react-ga4" import ReactGA from "react-ga4"
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom" import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
import Tinycon from "tinycon" import Tinycon from "tinycon"
function Providers(props: { children: React.ReactNode }) { function Providers(props: { children: React.ReactNode }) {
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
return ( return (
<I18nProvider i18n={i18n}> <I18nProvider i18n={i18n}>
<MantineProvider <MantineProvider
defaultColorScheme="auto" defaultColorScheme="auto"
theme={{ theme={{
primaryColor: "orange", primaryColor: primaryColor,
fontFamily: "Open Sans", fontFamily: "Open Sans",
colors: { colors: {
// keep using dark colors from mantine v6 // keep using dark colors from mantine v6
@@ -69,7 +72,7 @@ function Providers(props: { children: React.ReactNode }) {
) )
} }
// swagger-ui is very large, load only on-demand // api documentation page is very large, load only on-demand
const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage")) const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage"))
function AppRoutes() { function AppRoutes() {
@@ -140,16 +143,22 @@ function GoogleAnalyticsHandler() {
return null return null
} }
function FaviconHandler() { function UnreadCountTitleHandler({ enabled }: { enabled?: boolean }) {
const root = useAppSelector(state => state.tree.rootCategory) const root = useAppSelector(state => state.tree.rootCategory)
const unreadCount = categoryUnreadCount(root)
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title>
}
function UnreadCountFaviconHandler({ enabled }: { enabled?: boolean }) {
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCount = categoryUnreadCount(root)
useEffect(() => { useEffect(() => {
const unreadCount = categoryUnreadCount(root) if (enabled && unreadCount > 0) {
if (unreadCount === 0) {
Tinycon.reset()
} else {
Tinycon.setBubble(unreadCount) Tinycon.setBubble(unreadCount)
} else {
Tinycon.reset()
} }
}, [root]) }, [unreadCount, enabled])
return null return null
} }
@@ -166,14 +175,12 @@ function BrowserExtensionBadgeUnreadCountHandler() {
return null return null
} }
function CustomJs() { function CustomJsHandler() {
const scriptLoaded = useRef(false) const [scriptLoaded, setScriptLoaded] = useState(false)
const { loading } = useAppLoading()
// useDidUpdate is used instead of useEffect because we want to skip the first render useEffect(() => {
// the first render is the render of react-router, the routes are actually loaded in a second render if (scriptLoaded || loading) {
// we want the script to be executed when the first route is done loading
useDidUpdate(() => {
if (scriptLoaded.current) {
return return
} }
@@ -182,13 +189,13 @@ function CustomJs() {
script.async = true script.async = true
document.body.appendChild(script) document.body.appendChild(script)
scriptLoaded.current = true setScriptLoaded(true)
}) }, [scriptLoaded, loading])
return null return null
} }
function CustomCss() { function CustomCssHandler() {
useEffect(() => { useEffect(() => {
const link = document.createElement("link") const link = document.createElement("link")
link.rel = "stylesheet" link.rel = "stylesheet"
@@ -202,6 +209,8 @@ function CustomCss() {
export function App() { export function App() {
useI18n() useI18n()
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
useEffect(() => { useEffect(() => {
@@ -211,14 +220,22 @@ export function App() {
return ( return (
<Providers> <Providers>
<> <>
<FaviconHandler /> <UnreadCountTitleHandler enabled={unreadCountTitle} />
<UnreadCountFaviconHandler enabled={unreadCountFavicon} />
<BrowserExtensionBadgeUnreadCountHandler /> <BrowserExtensionBadgeUnreadCountHandler />
<CustomJsHandler />
<CustomCssHandler />
{/* disable pull-to-refresh as it messes with vertical scrolling
safari behaves weirdly when overscroll-behavior is set to none so we disable it only for other browsers
https://github.com/Athou/commafeed/issues/1168
*/}
{!isSafari && <DisablePullToRefresh />}
<HashRouter> <HashRouter>
<GoogleAnalyticsHandler /> <GoogleAnalyticsHandler />
<RedirectHandler /> <RedirectHandler />
<AppRoutes /> <AppRoutes />
<CustomJs />
<CustomCss />
</HashRouter> </HashRouter>
</> </>
</Providers> </Providers>

View File

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

View File

@@ -1,127 +1,137 @@
import axios, { type AxiosError } from "axios" import axios, { type AxiosError } from "axios"
import { import type {
type AddCategoryRequest, AddCategoryRequest,
type AdminSaveUserRequest, AdminSaveUserRequest,
type AuthenticationError, AuthenticationError,
type Category, Category,
type CategoryModificationRequest, CategoryModificationRequest,
type CollapseRequest, CollapseRequest,
type Entries, Entries,
type FeedInfo, FeedInfo,
type FeedInfoRequest, FeedInfoRequest,
type FeedModificationRequest, FeedModificationRequest,
type GetEntriesPaginatedRequest, GetEntriesPaginatedRequest,
type IDRequest, IDRequest,
type LoginRequest, LoginRequest,
type MarkRequest, MarkRequest,
type Metrics, Metrics,
type MultipleMarkRequest, MultipleMarkRequest,
type PasswordResetRequest, PasswordResetRequest,
type ProfileModificationRequest, ProfileModificationRequest,
type RegistrationRequest, RegistrationRequest,
type ServerInfo, ServerInfo,
type Settings, Settings,
type StarRequest, StarRequest,
type SubscribeRequest, SubscribeRequest,
type Subscription, Subscription,
type TagRequest, TagRequest,
type UserModel, UserModel,
} from "./types" } from "./types"
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true }) const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
response => response, response => response,
error => { error => {
if (isAuthenticationError(error)) { if (isAuthenticationError(error)) {
const data = error.response?.data const data = error.response?.data
window.location.hash = data?.allowRegistrations ? "/welcome" : "/login" window.location.hash = data?.allowRegistrations ? "/welcome" : "/login"
} }
throw error throw error
} }
) )
function isAuthenticationError(error: unknown): error is AxiosError<AuthenticationError> { function isAuthenticationError(error: unknown): error is AxiosError<AuthenticationError> {
return axios.isAxiosError(error) && !!error.response && [401, 403].includes(error.response.status) return axios.isAxiosError(error) && !!error.response && [401, 403].includes(error.response.status)
} }
export const client = { export const client = {
category: { category: {
getRoot: async () => await axiosInstance.get<Category>("category/get"), getRoot: async () => await axiosInstance.get<Category>("category/get"),
modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req), modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req),
collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req), collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req),
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("category/entries", { params: req }), getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("category/entries", { params: req }),
markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req), markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req),
add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req), add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req),
delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req), delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req),
}, },
entry: { entry: {
mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req), mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req),
markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req), markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req),
star: async (req: StarRequest) => await axiosInstance.post("entry/star", req), star: async (req: StarRequest) => await axiosInstance.post("entry/star", req),
getTags: async () => await axiosInstance.get<string[]>("entry/tags"), getTags: async () => await axiosInstance.get<string[]>("entry/tags"),
tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req), tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req),
}, },
feed: { feed: {
get: async (id: string) => await axiosInstance.get<Subscription>(`feed/get/${id}`), get: async (id: string) => await axiosInstance.get<Subscription>(`feed/get/${id}`),
modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req), modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req),
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("feed/entries", { params: req }), getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("feed/entries", { params: req }),
markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req), markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req),
fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post<FeedInfo>("feed/fetch", req), fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post<FeedInfo>("feed/fetch", req),
refreshAll: async () => await axiosInstance.get("feed/refreshAll"), refreshAll: async () => await axiosInstance.get("feed/refreshAll"),
subscribe: async (req: SubscribeRequest) => await axiosInstance.post<number>("feed/subscribe", req), subscribe: async (req: SubscribeRequest) => await axiosInstance.post<number>("feed/subscribe", req),
unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req), unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req),
importOpml: async (req: File) => { importOpml: async (req: File) => {
const formData = new FormData() const formData = new FormData()
formData.append("file", req) formData.append("file", req)
return await axiosInstance.post("feed/import", formData, { return await axiosInstance.post("feed/import", formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
}) })
}, },
}, },
user: { user: {
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req), login: async (req: LoginRequest) => {
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req), const formData = new URLSearchParams()
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req), formData.append("j_username", req.name)
getSettings: async () => await axiosInstance.get<Settings>("user/settings"), formData.append("j_password", req.password)
saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings), return await axiosInstance.post("j_security_check", formData, {
getProfile: async () => await axiosInstance.get<UserModel>("user/profile"), baseURL: ".",
saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req), headers: {
deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"), "Content-Type": "application/x-www-form-urlencoded",
}, },
server: { })
getServerInfos: async () => await axiosInstance.get<ServerInfo>("server/get"), },
}, register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
admin: { passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"), getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req), saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings),
deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req), getProfile: async () => await axiosInstance.get<UserModel>("user/profile"),
getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"), saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req),
}, deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"),
} },
server: {
/** getServerInfos: async () => await axiosInstance.get<ServerInfo>("server/get"),
* transform an error object to an array of strings that can be displayed to the user },
* @param err an error object (e.g. from axios) admin: {
* @returns an array of messages to show the user getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"),
*/ saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req),
export const errorToStrings = (err: unknown) => { deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req),
let strings: string[] = [] getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"),
},
if (axios.isAxiosError(err) && err.response) { }
if (typeof err.response.data === "string") strings.push(err.response.data)
if (isMessageError(err)) strings.push(err.response.data.message) /**
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors] * transform an error object to an array of strings that can be displayed to the user
} * @param err an error object (e.g. from axios)
* @returns an array of messages to show the user
return strings */
} export const errorToStrings = (err: unknown) => {
let strings: string[] = []
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data if (axios.isAxiosError(err) && err.response) {
} if (typeof err.response.data === "string") strings.push(err.response.data)
if (isMessageError(err)) strings.push(err.response.data.message)
function isMessageArrayError(err: AxiosError): err is AxiosError<{ errors: string[] }> { if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data }
}
return strings
}
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
}
function isMessageArrayError(err: AxiosError): err is AxiosError<{ errors: string[] }> {
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data
}

View File

@@ -1,112 +1,113 @@
import { t } from "@lingui/macro" import type { IconType } from "react-icons"
import { type IconType } from "react-icons" import { FaAt } from "react-icons/fa"
import { FaAt } from "react-icons/fa" import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si" import type { Category, Entry, SharingSettings } from "./types"
import { type Category, type Entry, type SharingSettings } from "./types"
const categories: Record<string, Omit<Category, "name">> = {
const categories: Record<string, Category> = { all: {
all: { id: "all",
id: "all", expanded: false,
name: t`All`, children: [],
expanded: false, feeds: [],
children: [], position: 0,
feeds: [], },
position: 0, starred: {
}, id: "starred",
starred: { expanded: false,
id: "starred", children: [],
name: t`Starred`, feeds: [],
expanded: false, position: 1,
children: [], },
feeds: [], }
position: 1,
}, const sharing: {
} [key in keyof SharingSettings]: {
label: string
const sharing: { icon: IconType
[key in keyof SharingSettings]: { color: `#${string}`
label: string url: (url: string, description: string) => string
icon: IconType }
color: `#${string}` } = {
url: (url: string, description: string) => string email: {
} label: "Email",
} = { icon: FaAt,
email: { color: "#000000",
label: "Email", url: (url, desc) => `mailto:?subject=${desc}&body=${url}`,
icon: FaAt, },
color: "#000000", gmail: {
url: (url, desc) => `mailto:?subject=${desc}&body=${url}`, label: "Gmail",
}, icon: SiGmail,
gmail: { color: "#EA4335",
label: "Gmail", url: (url, desc) => `https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su=${desc}&body=${url}`,
icon: SiGmail, },
color: "#EA4335", facebook: {
url: (url, desc) => `https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su=${desc}&body=${url}`, label: "Facebook",
}, icon: SiFacebook,
facebook: { color: "#1B74E4",
label: "Facebook", url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
icon: SiFacebook, },
color: "#1B74E4", twitter: {
url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`, label: "X",
}, icon: SiX,
twitter: { color: "#000000",
label: "Twitter", url: (url, desc) => `https://x.com/share?text=${desc}&url=${url}`,
icon: SiTwitter, },
color: "#1D9BF0", tumblr: {
url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`, label: "Tumblr",
}, icon: SiTumblr,
tumblr: { color: "#375672",
label: "Tumblr", url: (url, desc) => `https://www.tumblr.com/share/link?url=${url}&name=${desc}`,
icon: SiTumblr, },
color: "#375672", pocket: {
url: (url, desc) => `https://www.tumblr.com/share/link?url=${url}&name=${desc}`, label: "Pocket",
}, icon: SiPocket,
pocket: { color: "#EF4154",
label: "Pocket", url: (url, desc) => `https://getpocket.com/save?url=${url}&title=${desc}`,
icon: SiPocket, },
color: "#EF4154", instapaper: {
url: (url, desc) => `https://getpocket.com/save?url=${url}&title=${desc}`, label: "Instapaper",
}, icon: SiInstapaper,
instapaper: { color: "#010101",
label: "Instapaper", url: (url, desc) => `https://www.instapaper.com/hello2?url=${url}&title=${desc}`,
icon: SiInstapaper, },
color: "#010101", buffer: {
url: (url, desc) => `https://www.instapaper.com/hello2?url=${url}&title=${desc}`, label: "Buffer",
}, icon: SiBuffer,
buffer: { color: "#000000",
label: "Buffer", url: (url, desc) => `https://bufferapp.com/add?url=${url}&text=${desc}`,
icon: SiBuffer, },
color: "#000000", }
url: (url, desc) => `https://bufferapp.com/add?url=${url}&text=${desc}`,
}, export const Constants = {
} categories,
sharing,
export const Constants = { layout: {
categories, mobileBreakpoint: 992,
sharing, mobileBreakpointName: "md",
layout: { headerHeight: 60,
mobileBreakpoint: 992, entryMaxWidth: 650,
mobileBreakpointName: "md", isTopVisible: (div: HTMLElement) => {
headerHeight: 60, const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
entryMaxWidth: 650, return div.getBoundingClientRect().top >= (header?.bottom ?? 0)
isTopVisible: (div: HTMLElement) => { },
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect() isBottomVisible: (div: HTMLElement) => {
return div.getBoundingClientRect().top >= (header?.bottom ?? 0) const footer = document.getElementById(Constants.dom.footerId)?.getBoundingClientRect()
}, return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight)
isBottomVisible: (div: HTMLElement) => { },
const footer = document.getElementById(Constants.dom.footerId)?.getBoundingClientRect() },
return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight) dom: {
}, headerId: "header",
}, footerId: "footer",
dom: { entryId: (entry: Entry) => `entry-id-${entry.id}`,
headerId: "header", entryContextMenuId: (entry: Entry) => entry.id,
footerId: "footer", },
entryId: (entry: Entry) => `entry-id-${entry.id}`, theme: {
entryContextMenuId: (entry: Entry) => entry.id, defaultPrimaryColor: "orange",
}, },
tooltip: { tooltip: {
delay: 500, delay: 500,
}, },
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension", browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e", customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
} bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
}

View File

@@ -1,145 +1,140 @@
import { configureStore } from "@reduxjs/toolkit" import { configureStore } from "@reduxjs/toolkit"
import { type client } from "app/client" import { client } from "app/client"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks" import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
import { reducers, type RootState } from "app/store" import { type RootState, reducers } from "app/store"
import { type Entries, type Entry } from "app/types" import type { Entries, Entry } from "app/types"
import { type AxiosResponse } from "axios" import type { AxiosResponse } from "axios"
import { beforeEach, describe, expect, it, vi } from "vitest" import { beforeEach, describe, expect, it, vi } from "vitest"
import { mockReset } from "vitest-mock-extended"
vi.mock(import("app/client"))
const mockClient = await vi.hoisted(async () => {
const mockModule = await import("vitest-mock-extended") describe("entries", () => {
return mockModule.mockDeep<typeof client>() beforeEach(() => {
}) vi.resetAllMocks()
vi.mock("app/client", () => ({ client: mockClient })) })
describe("entries", () => { it("loads entries", async () => {
beforeEach(() => { vi.mocked(client.feed.getEntries).mockResolvedValue({
mockReset(mockClient) data: {
}) entries: [{ id: "3" } as Entry],
hasMore: false,
it("loads entries", async () => { name: "my-feed",
mockClient.feed.getEntries.mockResolvedValue({ errorCount: 3,
data: { feedLink: "https://mysite.com/feed",
entries: [{ id: "3" } as Entry], timestamp: 123,
hasMore: false, ignoredReadStatus: false,
name: "my-feed", },
errorCount: 3, } as AxiosResponse<Entries>)
feedLink: "https://mysite.com/feed",
timestamp: 123, const store = configureStore({ reducer: reducers })
ignoredReadStatus: false, const promise = store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true }))
},
} as AxiosResponse<Entries>) expect(store.getState().entries.source.type).toBe("feed")
expect(store.getState().entries.source.id).toBe("feed-id")
const store = configureStore({ reducer: reducers }) expect(store.getState().entries.entries).toStrictEqual([])
const promise = store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true })) expect(store.getState().entries.hasMore).toBe(true)
expect(store.getState().entries.sourceLabel).toBe("")
expect(store.getState().entries.source.type).toBe("feed") expect(store.getState().entries.sourceWebsiteUrl).toBe("")
expect(store.getState().entries.source.id).toBe("feed-id") expect(store.getState().entries.timestamp).toBeUndefined()
expect(store.getState().entries.entries).toStrictEqual([])
expect(store.getState().entries.hasMore).toBe(true) await promise
expect(store.getState().entries.sourceLabel).toBe("") expect(store.getState().entries.source.type).toBe("feed")
expect(store.getState().entries.sourceWebsiteUrl).toBe("") expect(store.getState().entries.source.id).toBe("feed-id")
expect(store.getState().entries.timestamp).toBeUndefined() expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }])
expect(store.getState().entries.hasMore).toBe(false)
await promise expect(store.getState().entries.sourceLabel).toBe("my-feed")
expect(store.getState().entries.source.type).toBe("feed") expect(store.getState().entries.sourceWebsiteUrl).toBe("https://mysite.com/feed")
expect(store.getState().entries.source.id).toBe("feed-id") expect(store.getState().entries.timestamp).toBe(123)
expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }]) })
expect(store.getState().entries.hasMore).toBe(false)
expect(store.getState().entries.sourceLabel).toBe("my-feed") it("loads more entries", async () => {
expect(store.getState().entries.sourceWebsiteUrl).toBe("https://mysite.com/feed") vi.mocked(client.category.getEntries).mockResolvedValue({
expect(store.getState().entries.timestamp).toBe(123) data: {
}) entries: [{ id: "4" } as Entry],
hasMore: false,
it("loads more entries", async () => { name: "my-feed",
mockClient.category.getEntries.mockResolvedValue({ errorCount: 3,
data: { feedLink: "https://mysite.com/feed",
entries: [{ id: "4" } as Entry], timestamp: 123,
hasMore: false, ignoredReadStatus: false,
name: "my-feed", },
errorCount: 3, } as AxiosResponse<Entries>)
feedLink: "https://mysite.com/feed",
timestamp: 123, const store = configureStore({
ignoredReadStatus: false, reducer: reducers,
}, preloadedState: {
} as AxiosResponse<Entries>) entries: {
source: {
const store = configureStore({ type: "category",
reducer: reducers, id: "category-id",
preloadedState: { },
entries: { sourceLabel: "",
source: { sourceWebsiteUrl: "",
type: "category", entries: [{ id: "3" } as Entry],
id: "category-id", hasMore: true,
}, loading: false,
sourceLabel: "", scrollingToEntry: false,
sourceWebsiteUrl: "", },
entries: [{ id: "3" } as Entry], } as RootState,
hasMore: true, })
loading: false, const promise = store.dispatch(loadMoreEntries())
scrollingToEntry: false,
}, await promise
} as RootState, expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }, { id: "4" }])
}) expect(store.getState().entries.hasMore).toBe(false)
const promise = store.dispatch(loadMoreEntries()) })
await promise it("marks an entry as read", () => {
expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }, { id: "4" }]) const store = configureStore({
expect(store.getState().entries.hasMore).toBe(false) reducer: reducers,
}) preloadedState: {
entries: {
it("marks an entry as read", () => { source: {
const store = configureStore({ type: "category",
reducer: reducers, id: "category-id",
preloadedState: { },
entries: { sourceLabel: "",
source: { sourceWebsiteUrl: "",
type: "category", entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
id: "category-id", hasMore: true,
}, loading: false,
sourceLabel: "", scrollingToEntry: false,
sourceWebsiteUrl: "", },
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], } as RootState,
hasMore: true, })
loading: false,
scrollingToEntry: false, store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true }))
}, expect(store.getState().entries.entries).toStrictEqual([
} as RootState, { id: "3", read: true },
}) { id: "4", read: false },
])
store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true })) expect(client.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
expect(store.getState().entries.entries).toStrictEqual([ })
{ id: "3", read: true },
{ id: "4", read: false }, it("marks all entries as read", () => {
]) const store = configureStore({
expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true }) reducer: reducers,
}) preloadedState: {
entries: {
it("marks all entries as read", () => { source: {
const store = configureStore({ type: "category",
reducer: reducers, id: "category-id",
preloadedState: { },
entries: { sourceLabel: "",
source: { sourceWebsiteUrl: "",
type: "category", entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
id: "category-id", hasMore: true,
}, loading: false,
sourceLabel: "", scrollingToEntry: false,
sourceWebsiteUrl: "", },
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], } as RootState,
hasMore: true, })
loading: false,
scrollingToEntry: false, store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } }))
}, expect(store.getState().entries.entries).toStrictEqual([
} as RootState, { id: "3", read: true },
}) { id: "4", read: true },
])
store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } })) expect(client.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true })
expect(store.getState().entries.entries).toStrictEqual([ })
{ id: "3", read: true }, })
{ id: "4", read: true },
])
expect(mockClient.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true })
})
})

View File

@@ -1,134 +1,127 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks" import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks"
import { type Entry } from "app/types" import type { Entry } from "app/types"
export type EntrySourceType = "category" | "feed" | "tag" export type EntrySourceType = "category" | "feed" | "tag"
export interface EntrySource { export interface EntrySource {
type: EntrySourceType type: EntrySourceType
id: string id: string
} }
export type ExpendableEntry = Entry & { expanded?: boolean } export type ExpendableEntry = Entry & { expanded?: boolean }
interface EntriesState { interface EntriesState {
/** selected source */ /** selected source */
source: EntrySource source: EntrySource
sourceLabel: string sourceLabel: string
sourceWebsiteUrl: string sourceWebsiteUrl: string
entries: ExpendableEntry[] entries: ExpendableEntry[]
/** stores when the first batch of entries were retrieved /** stores when the first batch of entries were retrieved
* *
* this is used when marking all entries of a feed/category to only mark entries up to that timestamp as newer entries were potentially never shown * this is used when marking all entries of a feed/category to only mark entries up to that timestamp as newer entries were potentially never shown
*/ */
timestamp?: number timestamp?: number
selectedEntryId?: string selectedEntryId?: string
hasMore: boolean hasMore: boolean
loading: boolean loading: boolean
search?: string search?: string
scrollingToEntry: boolean scrollingToEntry: boolean
} markAllAsReadConfirmationDialogOpen: boolean
}
const initialState: EntriesState = {
source: { const initialState: EntriesState = {
type: "category", source: {
id: Constants.categories.all.id, type: "category",
}, id: Constants.categories.all.id,
sourceLabel: "", },
sourceWebsiteUrl: "", sourceLabel: "",
entries: [], sourceWebsiteUrl: "",
hasMore: true, entries: [],
loading: false, hasMore: true,
scrollingToEntry: false, loading: false,
} scrollingToEntry: false,
markAllAsReadConfirmationDialogOpen: false,
export const entriesSlice = createSlice({ }
name: "entries",
initialState, export const entriesSlice = createSlice({
reducers: { name: "entries",
setSelectedEntry: (state, action: PayloadAction<Entry>) => { initialState,
state.selectedEntryId = action.payload.id reducers: {
}, setSelectedEntry: (state, action: PayloadAction<Entry>) => {
setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => { state.selectedEntryId = action.payload.id
state.entries },
.filter(e => e.id === action.payload.entry.id) setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => {
.forEach(e => { for (const e of state.entries.filter(e => e.id === action.payload.entry.id)) {
e.expanded = action.payload.expanded e.expanded = action.payload.expanded
}) }
}, },
setScrollingToEntry: (state, action: PayloadAction<boolean>) => { setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
state.scrollingToEntry = action.payload state.scrollingToEntry = action.payload
}, },
setSearch: (state, action: PayloadAction<string>) => { setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload state.search = action.payload
}, },
}, setMarkAllAsReadConfirmationDialogOpen: (state, action: PayloadAction<boolean>) => {
extraReducers: builder => { state.markAllAsReadConfirmationDialogOpen = action.payload
builder.addCase(markEntry.pending, (state, action) => { },
state.entries },
.filter(e => e.id === action.meta.arg.entry.id) extraReducers: builder => {
.forEach(e => { builder.addCase(markEntry.pending, (state, action) => {
e.read = action.meta.arg.read for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) {
}) e.read = action.meta.arg.read
}) }
builder.addCase(markMultipleEntries.pending, (state, action) => { })
state.entries builder.addCase(markMultipleEntries.pending, (state, action) => {
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id)) for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) {
.forEach(e => { e.read = action.meta.arg.read
e.read = action.meta.arg.read }
}) })
}) builder.addCase(markAllEntries.pending, (state, action) => {
builder.addCase(markAllEntries.pending, (state, action) => { for (const e of state.entries.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))) {
state.entries e.read = true
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true)) }
.forEach(e => { })
e.read = true builder.addCase(starEntry.pending, (state, action) => {
}) for (const e of state.entries.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)) {
}) e.starred = action.meta.arg.starred
builder.addCase(starEntry.pending, (state, action) => { }
state.entries })
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId) builder.addCase(loadEntries.pending, (state, action) => {
.forEach(e => { state.source = action.meta.arg.source
e.starred = action.meta.arg.starred state.entries = []
}) state.timestamp = undefined
}) state.sourceLabel = ""
builder.addCase(loadEntries.pending, (state, action) => { state.sourceWebsiteUrl = ""
state.source = action.meta.arg.source state.hasMore = true
state.entries = [] state.selectedEntryId = undefined
state.timestamp = undefined state.loading = true
state.sourceLabel = "" })
state.sourceWebsiteUrl = "" builder.addCase(loadMoreEntries.pending, state => {
state.hasMore = true state.loading = true
state.selectedEntryId = undefined })
state.loading = true builder.addCase(loadEntries.fulfilled, (state, action) => {
}) state.entries = action.payload.entries
builder.addCase(loadMoreEntries.pending, state => { state.timestamp = action.payload.timestamp
state.loading = true state.sourceLabel = action.payload.name
}) state.sourceWebsiteUrl = action.payload.feedLink
builder.addCase(loadEntries.fulfilled, (state, action) => { state.hasMore = action.payload.hasMore
state.entries = action.payload.entries state.loading = false
state.timestamp = action.payload.timestamp })
state.sourceLabel = action.payload.name builder.addCase(loadMoreEntries.fulfilled, (state, action) => {
state.sourceWebsiteUrl = action.payload.feedLink // remove already existing entries
state.hasMore = action.payload.hasMore const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id))
state.loading = false state.entries = [...state.entries, ...entriesToAdd]
}) state.hasMore = action.payload.hasMore
builder.addCase(loadMoreEntries.fulfilled, (state, action) => { state.loading = false
// remove already existing entries })
const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id)) builder.addCase(tagEntry.pending, (state, action) => {
state.entries = [...state.entries, ...entriesToAdd] for (const e of state.entries.filter(e => +e.id === action.meta.arg.entryId)) {
state.hasMore = action.payload.hasMore e.tags = action.meta.arg.tags
state.loading = false }
}) })
builder.addCase(tagEntry.pending, (state, action) => { },
state.entries })
.filter(e => +e.id === action.meta.arg.entryId)
.forEach(e => { export const { setSearch, setMarkAllAsReadConfirmationDialogOpen } = entriesSlice.actions
e.tags = action.meta.arg.tags
})
})
},
})
export const { setSearch } = entriesSlice.actions

View File

@@ -1,247 +1,307 @@
import { createAppAsyncThunk } from "app/async-thunk" import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client" import { client } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { entriesSlice, type EntrySource, type EntrySourceType, setSearch } from "app/entries/slice" import { type EntrySource, type EntrySourceType, entriesSlice, setMarkAllAsReadConfirmationDialogOpen, setSearch } from "app/entries/slice"
import type { RootState } from "app/store" import type { RootState } from "app/store"
import { reloadTree } from "app/tree/thunks" import { reloadTree } from "app/tree/thunks"
import type { Entry, MarkRequest, TagRequest } from "app/types" import type { Entry, MarkRequest, TagRequest } from "app/types"
import { reloadTags } from "app/user/thunks" import { reloadTags } from "app/user/thunks"
import { scrollToWithCallback } from "app/utils" import { scrollToWithCallback } from "app/utils"
import { flushSync } from "react-dom" import { flushSync } from "react-dom"
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
export const loadEntries = createAppAsyncThunk(
"entries/load", export const loadEntries = createAppAsyncThunk(
async ( "entries/load",
arg: { async (
source: EntrySource arg: {
clearSearch: boolean source: EntrySource
}, clearSearch: boolean
thunkApi },
) => { thunkApi
if (arg.clearSearch) thunkApi.dispatch(setSearch("")) ) => {
if (arg.clearSearch) thunkApi.dispatch(setSearch(""))
const state = thunkApi.getState()
const endpoint = getEndpoint(arg.source.type) const state = thunkApi.getState()
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0)) const endpoint = getEndpoint(arg.source.type)
return result.data const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
} return result.data
) }
export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => { )
const state = thunkApi.getState()
const { source } = state.entries export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => {
const offset = const state = thunkApi.getState()
state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length const { source } = state.entries
const endpoint = getEndpoint(state.entries.source.type) const offset =
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset)) state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length
return result.data const endpoint = getEndpoint(state.entries.source.type)
}) const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({ return result.data
id: source.type === "tag" ? Constants.categories.all.id : source.id, })
order: state.user.settings?.readingOrder,
readType: state.user.settings?.readingMode, const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
offset, id: source.type === "tag" ? Constants.categories.all.id : source.id,
limit: 50, order: state.user.settings?.readingOrder,
tag: source.type === "tag" ? source.id : undefined, readType: state.entries.search ? "all" : state.user.settings?.readingMode,
keywords: state.entries.search, offset,
}) limit: 50,
export const reloadEntries = createAppAsyncThunk("entries/reload", (arg, thunkApi) => { tag: source.type === "tag" ? source.id : undefined,
const state = thunkApi.getState() keywords: state.entries.search,
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) })
})
export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => { export const reloadEntries = createAppAsyncThunk("entries/reload", (_, thunkApi) => {
const state = thunkApi.getState() const state = thunkApi.getState()
thunkApi.dispatch(setSearch(arg)) thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) })
})
export const markEntry = createAppAsyncThunk( export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => {
"entries/entry/mark", const state = thunkApi.getState()
(arg: { entry: Entry; read: boolean }) => { thunkApi.dispatch(setSearch(arg))
client.entry.mark({ thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
id: arg.entry.id, })
read: arg.read,
}) export const markEntry = createAppAsyncThunk(
}, "entries/entry/mark",
{ (arg: { entry: Entry; read: boolean }) => {
condition: arg => arg.entry.markable && arg.entry.read !== arg.read, client.entry.mark({
} id: arg.entry.id,
) read: arg.read,
export const markMultipleEntries = createAppAsyncThunk( })
"entries/entry/markMultiple", },
async ( {
arg: { condition: arg => arg.entry.markable && arg.entry.read !== arg.read,
entries: Entry[] }
read: boolean )
},
thunkApi export const markMultipleEntries = createAppAsyncThunk(
) => { "entries/entry/markMultiple",
const requests: MarkRequest[] = arg.entries.map(e => ({ async (
id: e.id, arg: {
read: arg.read, entries: Entry[]
})) read: boolean
await client.entry.markMultiple({ requests }) },
thunkApi.dispatch(reloadTree()) thunkApi
} ) => {
) const requests: MarkRequest[] = arg.entries.map(e => ({
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => { id: e.id,
const state = thunkApi.getState() read: arg.read,
const { entries } = state.entries }))
await client.entry.markMultiple({ requests })
const index = entries.findIndex(e => e.id === arg.id) thunkApi.dispatch(reloadTree())
if (index === -1) return }
)
thunkApi.dispatch(
markMultipleEntries({ export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => {
entries: entries.slice(0, index + 1), const state = thunkApi.getState()
read: true, const { entries } = state.entries
})
) const index = entries.findIndex(e => e.id === arg.id)
}) if (index === -1) return
export const markAllEntries = createAppAsyncThunk(
"entries/entry/markAll", thunkApi.dispatch(
async ( markMultipleEntries({
arg: { entries: entries.slice(0, index + 1),
sourceType: EntrySourceType read: true,
req: MarkRequest })
}, )
thunkApi })
) => {
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries export const markAllEntries = createAppAsyncThunk(
await endpoint(arg.req) "entries/entry/markAll",
thunkApi.dispatch(reloadEntries()) async (
thunkApi.dispatch(reloadTree()) arg: {
} sourceType: EntrySourceType
) req: MarkRequest
export const starEntry = createAppAsyncThunk( },
"entries/entry/star", thunkApi
(arg: { entry: Entry; starred: boolean }) => { ) => {
client.entry.star({ const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
id: arg.entry.id, await endpoint(arg.req)
feedId: +arg.entry.feedId, thunkApi.dispatch(reloadEntries())
starred: arg.starred, thunkApi.dispatch(reloadTree())
}) }
}, )
{
condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred, export const markAllAsReadWithConfirmationIfRequired = createAppAsyncThunk(
} "entries/entry/markAllAsReadWithConfirmationIfRequired",
) async (_, thunkApi) => {
export const selectEntry = createAppAsyncThunk( const state = thunkApi.getState()
"entries/entry/select", const source = state.entries.source
( const entriesTimestamp = state.entries.timestamp ?? Date.now()
arg: { const markAllAsReadConfirmation = state.user.settings?.markAllAsReadConfirmation
entry: Entry
expand: boolean if (markAllAsReadConfirmation) {
markAsRead: boolean thunkApi.dispatch(setMarkAllAsReadConfirmationDialogOpen(true))
scrollToEntry: boolean } else {
}, thunkApi.dispatch(
thunkApi markAllEntries({
) => { sourceType: source.type,
const state = thunkApi.getState() req: {
const entry = state.entries.entries.find(e => e.id === arg.entry.id) id: source.id,
if (!entry) return read: true,
olderThan: Date.now(),
// flushSync is required because we need the newly selected entry to be expanded insertedBefore: entriesTimestamp,
// and the previously selected entry to be collapsed to be able to scroll to the right position },
flushSync(() => { })
// mark as read if requested )
if (arg.markAsRead) { }
thunkApi.dispatch(markEntry({ entry, read: true })) }
} )
// set entry as selected export const starEntry = createAppAsyncThunk(
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry)) "entries/entry/star",
(arg: { entry: Entry; starred: boolean }) => {
// expand if requested client.entry.star({
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId) id: arg.entry.id,
if (previouslySelectedEntry) { feedId: +arg.entry.feedId,
thunkApi.dispatch( starred: arg.starred,
entriesSlice.actions.setEntryExpanded({ })
entry: previouslySelectedEntry, },
expanded: false, {
}) condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred,
) }
} )
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
}) export const selectEntry = createAppAsyncThunk(
"entries/entry/select",
if (arg.scrollToEntry) { (
const entryElement = document.getElementById(Constants.dom.entryId(entry)) arg: {
if (entryElement) { entry: Entry
const scrollMode = state.user.settings?.scrollMode expand: boolean
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement) markAsRead: boolean
if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) { scrollToEntry: boolean
const scrollSpeed = state.user.settings?.scrollSpeed },
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true)) thunkApi
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))) ) => {
} const state = thunkApi.getState()
} const entry = state.entries.entries.find(e => e.id === arg.entry.id)
} if (!entry) return
}
) // flushSync is required because we need the newly selected entry to be expanded
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => { // and the previously selected entry to be collapsed to be able to scroll to the right position
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect() flushSync(() => {
const offset = (header?.bottom ?? 0) + 3 // mark as read if requested
scrollToWithCallback({ if (arg.markAsRead) {
options: { thunkApi.dispatch(markEntry({ entry, read: true }))
top: entryElement.offsetTop - offset, }
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
}, // set entry as selected
onScrollEnded, thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
})
} // expand if requested
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
export const selectPreviousEntry = createAppAsyncThunk( if (previouslySelectedEntry) {
"entries/entry/selectPrevious", thunkApi.dispatch(
( entriesSlice.actions.setEntryExpanded({
arg: { entry: previouslySelectedEntry,
expand: boolean expanded: false,
markAsRead: boolean })
scrollToEntry: boolean )
}, }
thunkApi thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
) => { })
const state = thunkApi.getState()
const { entries } = state.entries if (arg.scrollToEntry) {
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1 const viewMode = state.user.localSettings.viewMode
if (previousIndex >= 0) {
thunkApi.dispatch( const entryIndex = state.entries.entries.indexOf(entry)
selectEntry({ const entriesToKeepOnTopWhenScrolling =
entry: entries[previousIndex], viewMode === "expanded" ? 0 : Math.min(state.user.settings?.entriesToKeepOnTopWhenScrolling ?? 0, entryIndex)
expand: arg.expand, const entryToScrollTo = state.entries.entries[entryIndex - entriesToKeepOnTopWhenScrolling]
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry, const entryElement = document.getElementById(Constants.dom.entryId(entry))
}) const entryElementToScrollTo = document.getElementById(Constants.dom.entryId(entryToScrollTo))
) if (entryElement && entryElementToScrollTo) {
} const scrollMode = state.user.settings?.scrollMode
} const entryEntirelyVisible =
) Constants.layout.isTopVisible(entryElementToScrollTo) && Constants.layout.isBottomVisible(entryElement)
export const selectNextEntry = createAppAsyncThunk( if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) {
"entries/entry/selectNext", const scrollSpeed = state.user.settings?.scrollSpeed
( const margin = viewMode === "detailed" ? 8 : 3
arg: { thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
expand: boolean scrollToEntry(entryElementToScrollTo, margin, scrollSpeed, () =>
markAsRead: boolean thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))
scrollToEntry: boolean )
}, }
thunkApi }
) => { }
const state = thunkApi.getState() }
const { entries } = state.entries )
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
if (nextIndex < entries.length) { const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
thunkApi.dispatch( const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
selectEntry({ const offset = (header?.bottom ?? 0) + margin
entry: entries[nextIndex], scrollToWithCallback({
expand: arg.expand, options: {
markAsRead: arg.markAsRead, top: entryElement.offsetTop - offset,
scrollToEntry: arg.scrollToEntry, behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
}) },
) onScrollEnded,
} })
} }
)
export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => { export const selectPreviousEntry = createAppAsyncThunk(
await client.entry.tag(arg) "entries/entry/selectPrevious",
thunkApi.dispatch(reloadTags()) (
}) arg: {
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
thunkApi
) => {
const state = thunkApi.getState()
const { entries } = state.entries
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
if (previousIndex >= 0) {
thunkApi.dispatch(
selectEntry({
entry: entries[previousIndex],
expand: arg.expand,
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry,
})
)
}
}
)
export const selectNextEntry = createAppAsyncThunk(
"entries/entry/selectNext",
async (
arg: {
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
thunkApi
) => {
const state = thunkApi.getState()
const { entries, hasMore, loading } = state.entries
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
// load more entries if needed
// this can happen if the last entry is too large to fit on the screen and the infinite loader doesn't trigger
if (nextIndex >= entries.length && hasMore && !loading) {
await thunkApi.dispatch(loadMoreEntries())
}
const entriesAfterLoading = thunkApi.getState().entries.entries
if (nextIndex < entriesAfterLoading.length) {
thunkApi.dispatch(
selectEntry({
entry: entriesAfterLoading[nextIndex],
expand: arg.expand,
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry,
})
)
}
}
)
export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => {
await client.entry.tag(arg)
thunkApi.dispatch(reloadTags())
})

View File

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

View File

@@ -1,19 +1,19 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
interface RedirectState { interface RedirectState {
to?: string to?: string
} }
const initialState: RedirectState = {} const initialState: RedirectState = {}
export const redirectSlice = createSlice({ export const redirectSlice = createSlice({
name: "redirect", name: "redirect",
initialState, initialState,
reducers: { reducers: {
redirectTo: (state, action: PayloadAction<string | undefined>) => { redirectTo: (state, action: PayloadAction<string | undefined>) => {
state.to = action.payload state.to = action.payload
}, },
}, },
}) })
export const { redirectTo } = redirectSlice.actions export const { redirectTo } = redirectSlice.actions

View File

@@ -1,45 +1,57 @@
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 redirectToPasswordRecovery = createAppAsyncThunk("redirect/passwordRecovery", (_, thunkApi) => export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
thunkApi.dispatch(redirectTo("/passwordRecovery"))
) export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => { const { source } = thunkApi.getState().entries
const { source } = thunkApi.getState().entries thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`)) })
})
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) => export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/category/${id}`)) thunkApi.dispatch(redirectTo(`/app/category/${id}`))
) )
export const redirectToRootCategory = createAppAsyncThunk(
"redirect/category/root", export const redirectToRootCategory = createAppAsyncThunk(
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id)) "redirect/category/root",
) async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) => )
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
) export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) => thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
thunkApi.dispatch(redirectTo(`/app/feed/${id}`)) )
)
export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) => export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`)) thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
) )
export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) => export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`)) thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
) )
export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings"))) export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/users")) export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
) thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) => )
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
) export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about"))) export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/users"))
)
export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
)
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))

View File

@@ -1,29 +1,29 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
import { reloadServerInfos } from "app/server/thunks" import { reloadServerInfos } from "app/server/thunks"
import { type ServerInfo } from "app/types" import type { ServerInfo } from "app/types"
interface ServerState { interface ServerState {
serverInfos?: ServerInfo serverInfos?: ServerInfo
webSocketConnected: boolean webSocketConnected: boolean
} }
const initialState: ServerState = { const initialState: ServerState = {
webSocketConnected: false, webSocketConnected: false,
} }
export const serverSlice = createSlice({ export const serverSlice = createSlice({
name: "server", name: "server",
initialState, initialState,
reducers: { reducers: {
setWebSocketConnected: (state, action: PayloadAction<boolean>) => { setWebSocketConnected: (state, action: PayloadAction<boolean>) => {
state.webSocketConnected = action.payload state.webSocketConnected = action.payload
}, },
}, },
extraReducers: builder => { extraReducers: builder => {
builder.addCase(reloadServerInfos.fulfilled, (state, action) => { builder.addCase(reloadServerInfos.fulfilled, (state, action) => {
state.serverInfos = action.payload state.serverInfos = action.payload
}) })
}, },
}) })
export const { setWebSocketConnected } = serverSlice.actions export const { setWebSocketConnected } = serverSlice.actions

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,23 +1,43 @@
import { configureStore } from "@reduxjs/toolkit" import { configureStore } from "@reduxjs/toolkit"
import { entriesSlice } from "app/entries/slice" import { entriesSlice } from "app/entries/slice"
import { redirectSlice } from "app/redirect/slice" import { redirectSlice } from "app/redirect/slice"
import { serverSlice } from "app/server/slice" import { serverSlice } from "app/server/slice"
import { treeSlice } from "app/tree/slice" import { treeSlice } from "app/tree/slice"
import { userSlice } from "app/user/slice" import type { LocalSettings } from "app/types"
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" import { initialLocalSettings, userSlice } from "app/user/slice"
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
export const reducers = {
entries: entriesSlice.reducer, export const reducers = {
redirect: redirectSlice.reducer, entries: entriesSlice.reducer,
tree: treeSlice.reducer, redirect: redirectSlice.reducer,
server: serverSlice.reducer, tree: treeSlice.reducer,
user: userSlice.reducer, server: serverSlice.reducer,
} user: userSlice.reducer,
}
export const store = configureStore({ reducer: reducers })
const loadLocalSettings = (): LocalSettings => {
export type RootState = ReturnType<typeof store.getState> const json = localStorage.getItem("commafeed-local-settings")
export type AppDispatch = typeof store.dispatch return {
...initialLocalSettings,
export const useAppDispatch: () => AppDispatch = useDispatch ...(json ? JSON.parse(json) : {}),
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector }
}
export const store = configureStore({
reducer: reducers,
preloadedState: {
user: {
localSettings: loadLocalSettings(),
},
},
})
store.subscribe(() => {
const localSettings = store.getState().user.localSettings
localStorage.setItem("commafeed-local-settings", JSON.stringify(localSettings))
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@@ -1,72 +1,112 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { type PayloadAction, createSlice } 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"
interface TreeState { export interface TreeSubscription extends Subscription {
rootCategory?: Category // client-side only flag
mobileMenuOpen: boolean hasNewEntries?: boolean
sidebarVisible: boolean }
}
export interface TreeCategory extends Category {
const initialState: TreeState = { feeds: TreeSubscription[]
mobileMenuOpen: false, children: TreeCategory[]
sidebarVisible: true, }
}
interface TreeState {
export const treeSlice = createSlice({ rootCategory?: TreeCategory
name: "tree", mobileMenuOpen: boolean
initialState, sidebarVisible: boolean
reducers: { }
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
state.mobileMenuOpen = action.payload const initialState: TreeState = {
}, mobileMenuOpen: false,
toggleSidebar: state => { sidebarVisible: true,
state.sidebarVisible = !state.sidebarVisible }
},
incrementUnreadCount: ( export const treeSlice = createSlice({
state, name: "tree",
action: PayloadAction<{ initialState,
feedId: number reducers: {
amount: number setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
}> state.mobileMenuOpen = action.payload
) => { },
if (!state.rootCategory) return toggleSidebar: state => {
visitCategoryTree(state.rootCategory, c => state.sidebarVisible = !state.sidebarVisible
c.feeds },
.filter(f => f.id === action.payload.feedId) incrementUnreadCount: (
.forEach(f => { state,
f.unread += action.payload.amount action: PayloadAction<{
}) feedId: number
) amount: number
}, }>
}, ) => {
extraReducers: builder => { if (!state.rootCategory) return
builder.addCase(reloadTree.fulfilled, (state, action) => { visitCategoryTree(state.rootCategory, c => {
state.rootCategory = action.payload for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) {
}) f.unread += action.payload.amount
builder.addCase(collapseTreeCategory.pending, (state, action) => { f.hasNewEntries = true
if (!state.rootCategory) return }
visitCategoryTree(state.rootCategory, c => { })
if (+c.id === action.meta.arg.id) c.expanded = !action.meta.arg.collapse },
}) },
}) extraReducers: builder => {
builder.addCase(markEntry.pending, (state, action) => { builder.addCase(reloadTree.fulfilled, (state, action) => {
if (!state.rootCategory) return // set hasNewEntries to true if new unread > previous unread
visitCategoryTree(state.rootCategory, c => if (state.rootCategory) {
c.feeds const oldFeeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
.filter(f => f.id === +action.meta.arg.entry.feedId) const oldFeedsById = new Map(oldFeeds.map(f => [f.id, f]))
.forEach(f => {
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1 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) {
builder.addCase(redirectTo, state => { newFeed.hasNewEntries = true
state.mobileMenuOpen = false }
}) }
}, }
})
state.rootCategory = action.payload
export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions })
builder.addCase(collapseTreeCategory.pending, (state, action) => {
if (!state.rootCategory) return
visitCategoryTree(state.rootCategory, c => {
if (+c.id === action.meta.arg.id) c.expanded = !action.meta.arg.collapse
})
})
builder.addCase(markEntry.pending, (state, action) => {
if (!state.rootCategory) return
visitCategoryTree(state.rootCategory, c => {
for (const f of c.feeds.filter(f => f.id === +action.meta.arg.entry.feedId)) {
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
}
})
})
builder.addCase(loadEntries.fulfilled, (state, action) => {
if (!state.rootCategory) return
const { source } = action.meta.arg
if (source.type === "category") {
visitCategoryTree(state.rootCategory, c => {
if (c.id === source.id) {
for (const f of flattenCategoryTree(c).flatMap(c => c.feeds)) {
f.hasNewEntries = false
}
}
})
} else if (source.type === "feed") {
const feeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
for (const f of feeds.filter(f => f.id === +source.id)) {
f.hasNewEntries = false
}
}
})
builder.addCase(redirectTo, state => {
state.mobileMenuOpen = false
})
},
})
export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions

View File

@@ -1,9 +1,80 @@
import { createAppAsyncThunk } from "app/async-thunk" import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client" import { client } from "app/client"
import type { CollapseRequest } from "app/types" import { redirectToCategory, redirectToFeed } from "app/redirect/thunks"
import { incrementUnreadCount } from "app/tree/slice"
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data)) import type { CollapseRequest, Subscription } from "app/types"
export const collapseTreeCategory = createAppAsyncThunk( import { flattenCategoryTree, visitCategoryTree } from "app/utils"
"tree/category/collapse",
async (req: CollapseRequest) => await client.category.collapse(req) export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
)
export const collapseTreeCategory = createAppAsyncThunk(
"tree/category/collapse",
async (req: CollapseRequest) => await client.category.collapse(req).then(r => r.data)
)
export const selectNextUnreadTreeItem = createAppAsyncThunk(
"tree/selectNextUnreadItem",
(
arg: {
direction: "forward" | "backward"
},
thunkApi
) => {
const state = thunkApi.getState()
const root = state.tree.rootCategory
if (!root) return
const { source } = state.entries
if (source.type === "category") {
const categories = flattenCategoryTree(root)
if (arg.direction === "backward") categories.reverse()
const index = categories.findIndex(c => c.id === source.id)
if (index === -1) return
for (let i = index + 1; i < categories.length; i++) {
const c = categories[i]
if (c.feeds.some(f => f.unread > 0)) {
return thunkApi.dispatch(redirectToCategory(String(c.id)))
}
}
} else if (source.type === "feed") {
const feeds: Subscription[] = []
visitCategoryTree(root, c => feeds.push(...c.feeds), { childrenFirst: true })
if (arg.direction === "backward") feeds.reverse()
const index = feeds.findIndex(f => f.id === +source.id)
if (index === -1) return
for (let i = index + 1; i < feeds.length; i++) {
const f = feeds[i]
if (f.unread > 0) {
return thunkApi.dispatch(redirectToFeed(String(f.id)))
}
}
}
}
)
export const newFeedEntriesDiscovered = createAppAsyncThunk(
"tree/new-feed-entries-discovered",
async ({ feedId, amount }: { feedId: number; amount: number }, thunkApi) => {
const root = thunkApi.getState().tree.rootCategory
if (!root) return
const feed = flattenCategoryTree(root)
.flatMap(c => c.feeds)
.some(f => f.id === feedId)
if (!feed) {
// feed not found in the tree, reload the tree completely
thunkApi.dispatch(reloadTree())
} else {
thunkApi.dispatch(
incrementUnreadCount({
feedId,
amount,
})
)
}
}
)

View File

@@ -0,0 +1,172 @@
import { configureStore } from "@reduxjs/toolkit"
import { client } from "app/client"
import { loadEntries } from "app/entries/thunks"
import { type RootState, reducers } from "app/store"
import { newFeedEntriesDiscovered, selectNextUnreadTreeItem } from "app/tree/thunks"
import type { Category, Entries, Entry, Subscription } from "app/types"
import type { AxiosResponse } from "axios"
import { beforeEach, describe, expect, it, vi } from "vitest"
vi.mock(import("app/client"))
const createCategory = (id: string): Category => ({
id,
name: id,
children: [],
feeds: [],
expanded: true,
position: 0,
})
const createFeed = (id: number, unread: number): Subscription => ({
id,
name: String(id),
unread,
errorCount: 0,
position: 0,
feedUrl: "",
feedLink: "",
iconUrl: "",
})
const root = createCategory("root")
const catA = createCategory("catA")
catA.feeds.push(createFeed(1, 0), createFeed(2, 0), createFeed(3, 1))
const catB = createCategory("catB")
const catC = createCategory("catC")
catC.feeds.push(createFeed(4, 1))
root.children.push(catA, catB, catC)
describe("selectNextUnreadTreeItem", () => {
it("selects the next unread category", async () => {
const store = configureStore({
reducer: reducers,
preloadedState: {
tree: {
rootCategory: root,
},
entries: {
source: {
type: "category",
id: "catA",
},
},
} as RootState,
})
await store.dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
expect(store.getState().redirect.to).toBe("/app/category/catC")
})
it("selects the previous unread category", async () => {
const store = configureStore({
reducer: reducers,
preloadedState: {
tree: {
rootCategory: root,
},
entries: {
source: {
type: "category",
id: "catC",
},
},
} as RootState,
})
await store.dispatch(selectNextUnreadTreeItem({ direction: "backward" }))
expect(store.getState().redirect.to).toBe("/app/category/catA")
})
it("selects the next unread feed", async () => {
const store = configureStore({
reducer: reducers,
preloadedState: {
tree: {
rootCategory: root,
},
entries: {
source: {
type: "feed",
id: "1",
},
},
} as RootState,
})
await store.dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
expect(store.getState().redirect.to).toBe("/app/feed/3")
})
it("selects the previous unread feed", async () => {
const store = configureStore({
reducer: reducers,
preloadedState: {
tree: {
rootCategory: root,
},
entries: {
source: {
type: "feed",
id: "4",
},
},
} as RootState,
})
await store.dispatch(selectNextUnreadTreeItem({ direction: "backward" }))
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

@@ -1,296 +1,308 @@
export type ReadingMode = "all" | "unread" export type ReadingMode = "all" | "unread"
export type ReadingOrder = "asc" | "desc" export type ReadingOrder = "asc" | "desc"
export type ViewMode = "title" | "cozy" | "detailed" | "expanded" export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
export type ScrollMode = "always" | "never" | "if_needed" export type ScrollMode = "always" | "never" | "if_needed"
export type IconDisplayMode = "always" | "never" | "on_desktop" | "on_mobile" export type IconDisplayMode = "always" | "never" | "on_desktop" | "on_mobile"
export interface AddCategoryRequest { export interface AddCategoryRequest {
name: string name: string
parentId?: string parentId?: string
} }
export interface Subscription { export interface Subscription {
id: number id: number
name: string name: string
message?: string message?: string
errorCount: number errorCount: number
lastRefresh?: number lastRefresh?: number
nextRefresh?: number nextRefresh?: number
feedUrl: string feedUrl: string
feedLink: string feedLink: string
iconUrl: string iconUrl: string
unread: number unread: number
categoryId?: string categoryId?: string
position: number position: number
newestItemTime?: number newestItemTime?: number
filter?: string filter?: string
} }
export interface Category { export interface Category {
id: string id: string
parentId?: string parentId?: string
parentName?: string parentName?: string
name: string name: string
children: Category[] children: Category[]
feeds: Subscription[] feeds: Subscription[]
expanded: boolean expanded: boolean
position: number position: number
} }
export interface CategoryModificationRequest { export interface CategoryModificationRequest {
id: number id: number
name?: string name?: string
parentId?: string parentId?: string
position?: number position?: number
} }
export interface CollapseRequest { export interface CollapseRequest {
id: number id: number
collapse: boolean collapse: boolean
} }
export interface Entry { export interface Entry {
id: string id: string
guid: string guid: string
title: string title: string
content: string content: string
categories?: string categories?: string
rtl: boolean rtl: boolean
author?: string author?: string
enclosureUrl?: string enclosureUrl?: string
enclosureType?: string enclosureType?: string
mediaDescription?: string mediaDescription?: string
mediaThumbnailUrl?: string mediaThumbnailUrl?: string
mediaThumbnailWidth?: number mediaThumbnailWidth?: number
mediaThumbnailHeight?: number mediaThumbnailHeight?: number
date: number date: number
insertedDate: number insertedDate: number
feedId: string feedId: string
feedName: string feedName: string
feedUrl: string feedUrl: string
feedLink: string feedLink: string
iconUrl: string iconUrl: string
url: string url: string
read: boolean read: boolean
starred: boolean starred: boolean
markable: boolean markable: boolean
tags: string[] tags: string[]
} }
export interface Entries { export interface Entries {
name: string name: string
message?: string message?: string
errorCount: number errorCount: number
feedLink: string feedLink: string
timestamp: number timestamp: number
hasMore: boolean hasMore: boolean
offset?: number offset?: number
limit?: number limit?: number
entries: Entry[] entries: Entry[]
ignoredReadStatus: boolean ignoredReadStatus: boolean
} }
export interface FeedInfo { export interface FeedInfo {
url: string url: string
title: string title: string
} }
export interface FeedInfoRequest { export interface FeedInfoRequest {
url: string url: string
} }
export interface FeedModificationRequest { export interface FeedModificationRequest {
id: number id: number
name?: string name?: string
categoryId?: string categoryId?: string
position?: number position?: number
filter?: string filter?: string
} }
export interface GetEntriesRequest { export interface GetEntriesRequest {
id: string id: string
readType?: ReadingMode readType?: ReadingMode
newerThan?: number newerThan?: number
order?: ReadingOrder order?: ReadingOrder
keywords?: string keywords?: string
onlyIds?: boolean excludedSubscriptionIds?: string
excludedSubscriptionIds?: string tag?: string
tag?: string }
}
export interface GetEntriesPaginatedRequest extends GetEntriesRequest {
export interface GetEntriesPaginatedRequest extends GetEntriesRequest { offset: number
offset: number limit: number
limit: number }
}
export interface IDRequest {
export interface IDRequest { id: number
id: number }
}
export interface LoginRequest {
export interface LoginRequest { name: string
name: string password: string
password: string }
}
export interface MarkRequest {
export interface MarkRequest { id: string
id: string read: boolean
read: boolean olderThan?: number
olderThan?: number insertedBefore?: number
insertedBefore?: number keywords?: string
keywords?: string excludedSubscriptions?: number[]
excludedSubscriptions?: number[] }
}
export interface MetricCounter {
export interface MetricCounter { count: number
count: number }
}
export interface MetricGauge {
export interface MetricGauge { value: number
value: number }
}
export interface MetricMeter {
export interface MetricMeter { count: number
count: number m15_rate: number
m15_rate: number m1_rate: number
m1_rate: number m5_rate: number
m5_rate: number mean_rate: number
mean_rate: number units: string
units: string }
}
export interface MetricTimer {
export interface MetricTimer { count: number
count: number max: number
max: number mean: number
mean: number min: number
min: number p50: number
p50: number p75: number
p75: number p95: number
p95: number p98: number
p98: number p99: number
p99: number p999: number
p999: number stddev: number
stddev: number m15_rate: number
m15_rate: number m1_rate: number
m1_rate: number m5_rate: number
m5_rate: number mean_rate: number
mean_rate: number duration_units: string
duration_units: string rate_units: string
rate_units: string }
}
export interface Metrics {
export interface Metrics { counters: Record<string, MetricCounter>
counters: Record<string, MetricCounter> gauges: Record<string, MetricGauge>
gauges: Record<string, MetricGauge> meters: Record<string, MetricMeter>
meters: Record<string, MetricMeter> timers: Record<string, MetricTimer>
timers: Record<string, MetricTimer> }
}
export interface MultipleMarkRequest {
export interface MultipleMarkRequest { requests: MarkRequest[]
requests: MarkRequest[] }
}
export interface PasswordResetRequest {
export interface PasswordResetRequest { email: string
email: string }
}
export interface ProfileModificationRequest {
export interface ProfileModificationRequest { currentPassword: string
currentPassword: string email: string
email: string newPassword?: string
newPassword?: string newApiKey?: boolean
newApiKey?: boolean }
}
export interface RegistrationRequest {
export interface RegistrationRequest { name: string
name: string password: string
password: string email: string
email: string }
}
export interface ServerInfo {
export interface ServerInfo { announcement?: string
announcement?: string version: string
version: string gitCommit: string
gitCommit: string allowRegistrations: boolean
allowRegistrations: boolean googleAnalyticsCode?: string
googleAnalyticsCode?: string smtpEnabled: boolean
smtpEnabled: boolean demoAccountEnabled: boolean
demoAccountEnabled: boolean websocketEnabled: boolean
websocketEnabled: boolean websocketPingInterval: number
websocketPingInterval: number treeReloadInterval: number
treeReloadInterval: number forceRefreshCooldownDuration: number
} }
export interface SharingSettings { export interface SharingSettings {
email: boolean email: boolean
gmail: boolean gmail: boolean
facebook: boolean facebook: boolean
twitter: boolean twitter: boolean
tumblr: boolean tumblr: boolean
pocket: boolean pocket: boolean
instapaper: boolean instapaper: boolean
buffer: boolean buffer: boolean
} }
export interface Settings { export interface Settings {
language: string language: string
readingMode: ReadingMode readingMode: ReadingMode
readingOrder: ReadingOrder readingOrder: ReadingOrder
showRead: boolean showRead: boolean
scrollMarks: boolean scrollMarks: boolean
customCss?: string customCss?: string
customJs?: string customJs?: string
scrollSpeed: number scrollSpeed: number
scrollMode: ScrollMode scrollMode: ScrollMode
starIconDisplayMode: IconDisplayMode entriesToKeepOnTopWhenScrolling: number
externalLinkIconDisplayMode: IconDisplayMode starIconDisplayMode: IconDisplayMode
markAllAsReadConfirmation: boolean externalLinkIconDisplayMode: IconDisplayMode
customContextMenu: boolean markAllAsReadConfirmation: boolean
mobileFooter: boolean customContextMenu: boolean
sharingSettings: SharingSettings mobileFooter: boolean
} unreadCountTitle: boolean
unreadCountFavicon: boolean
export interface StarRequest { primaryColor?: string
id: string sharingSettings: SharingSettings
feedId: number }
starred: boolean
} export interface LocalSettings {
viewMode: ViewMode
export interface SubscribeRequest { sidebarWidth: number
url: string announcementHash: string
title: string fontSizePercentage: number
categoryId?: string }
}
export interface StarRequest {
export interface TagRequest { id: string
entryId: number feedId: number
tags: string[] starred: boolean
} }
export interface UserModel { export interface SubscribeRequest {
id: number url: string
name: string title: string
email?: string categoryId?: string
apiKey?: string }
password?: string
enabled: boolean export interface TagRequest {
created: number entryId: number
lastLogin?: number tags: string[]
admin: boolean }
}
export interface UserModel {
export interface AdminSaveUserRequest { id: number
id?: number name: string
name: string email?: string
email?: string apiKey?: string
password?: string password?: string
enabled: boolean enabled: boolean
admin: boolean created: number
} lastLogin?: number
admin: boolean
export interface AuthenticationError { lastForceRefresh?: number
message: string }
allowRegistrations: boolean
} export interface AdminSaveUserRequest {
id?: number
name: string
email?: string
password?: string
enabled: boolean
admin: boolean
}
export interface AuthenticationError {
message: string
allowRegistrations: boolean
}

View File

@@ -1,120 +1,169 @@
import { t } from "@lingui/macro" import { t } from "@lingui/core/macro"
import { showNotification } from "@mantine/notifications" import { showNotification } from "@mantine/notifications"
import { createSlice, isAnyOf } from "@reduxjs/toolkit" import { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit"
import { type Settings, type UserModel } from "app/types" import type { LocalSettings, Settings, UserModel, ViewMode } from "app/types"
import { import {
changeCustomContextMenu, changeCustomContextMenu,
changeExternalLinkIconDisplayMode, changeEntriesToKeepOnTopWhenScrolling,
changeLanguage, changeExternalLinkIconDisplayMode,
changeMarkAllAsReadConfirmation, changeLanguage,
changeMobileFooter, changeMarkAllAsReadConfirmation,
changeReadingMode, changeMobileFooter,
changeReadingOrder, changePrimaryColor,
changeScrollMarks, changeReadingMode,
changeScrollMode, changeReadingOrder,
changeScrollSpeed, changeScrollMarks,
changeSharingSetting, changeScrollMode,
changeShowRead, changeScrollSpeed,
changeStarIconDisplayMode, changeSharingSetting,
reloadProfile, changeShowRead,
reloadSettings, changeStarIconDisplayMode,
reloadTags, changeUnreadCountFavicon,
} from "./thunks" changeUnreadCountTitle,
reloadProfile,
interface UserState { reloadSettings,
settings?: Settings reloadTags,
profile?: UserModel } from "./thunks"
tags?: string[]
} interface UserState {
settings?: Settings
const initialState: UserState = {} localSettings: LocalSettings
profile?: UserModel
export const userSlice = createSlice({ tags?: string[]
name: "user", }
initialState,
reducers: {}, export const initialLocalSettings: LocalSettings = {
extraReducers: builder => { viewMode: "detailed",
builder.addCase(reloadSettings.fulfilled, (state, action) => { sidebarWidth: 360,
state.settings = action.payload announcementHash: "no-hash",
}) fontSizePercentage: 100,
builder.addCase(reloadProfile.fulfilled, (state, action) => { }
state.profile = action.payload
}) const initialState: UserState = {
builder.addCase(reloadTags.fulfilled, (state, action) => { localSettings: initialLocalSettings,
state.tags = action.payload }
})
builder.addCase(changeReadingMode.pending, (state, action) => { export const userSlice = createSlice({
if (!state.settings) return name: "user",
state.settings.readingMode = action.meta.arg initialState,
}) reducers: {
builder.addCase(changeReadingOrder.pending, (state, action) => { setViewMode: (state, action: PayloadAction<ViewMode>) => {
if (!state.settings) return state.localSettings.viewMode = action.payload
state.settings.readingOrder = action.meta.arg },
}) setFontSizePercentage: (state, action: PayloadAction<number>) => {
builder.addCase(changeLanguage.pending, (state, action) => { state.localSettings.fontSizePercentage = action.payload
if (!state.settings) return },
state.settings.language = action.meta.arg setSidebarWidth: (state, action: PayloadAction<number>) => {
}) state.localSettings.sidebarWidth = action.payload
builder.addCase(changeScrollSpeed.pending, (state, action) => { },
if (!state.settings) return setAnnouncementHash: (state, action: PayloadAction<string>) => {
state.settings.scrollSpeed = action.meta.arg ? 400 : 0 state.localSettings.announcementHash = action.payload
}) },
builder.addCase(changeShowRead.pending, (state, action) => { },
if (!state.settings) return extraReducers: builder => {
state.settings.showRead = action.meta.arg builder.addCase(reloadSettings.fulfilled, (state, action) => {
}) state.settings = action.payload
builder.addCase(changeScrollMarks.pending, (state, action) => { })
if (!state.settings) return builder.addCase(reloadProfile.fulfilled, (state, action) => {
state.settings.scrollMarks = action.meta.arg state.profile = action.payload
}) })
builder.addCase(changeScrollMode.pending, (state, action) => { builder.addCase(reloadTags.fulfilled, (state, action) => {
if (!state.settings) return state.tags = action.payload
state.settings.scrollMode = action.meta.arg })
}) builder.addCase(changeReadingMode.pending, (state, action) => {
builder.addCase(changeStarIconDisplayMode.pending, (state, action) => { if (!state.settings) return
if (!state.settings) return state.settings.readingMode = action.meta.arg
state.settings.starIconDisplayMode = action.meta.arg })
}) builder.addCase(changeReadingOrder.pending, (state, action) => {
builder.addCase(changeExternalLinkIconDisplayMode.pending, (state, action) => { if (!state.settings) return
if (!state.settings) return state.settings.readingOrder = action.meta.arg
state.settings.externalLinkIconDisplayMode = action.meta.arg })
}) builder.addCase(changeLanguage.pending, (state, action) => {
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => { if (!state.settings) return
if (!state.settings) return state.settings.language = action.meta.arg
state.settings.markAllAsReadConfirmation = action.meta.arg })
}) builder.addCase(changeScrollSpeed.pending, (state, action) => {
builder.addCase(changeCustomContextMenu.pending, (state, action) => { if (!state.settings) return
if (!state.settings) return state.settings.scrollSpeed = action.meta.arg ? 400 : 0
state.settings.customContextMenu = action.meta.arg })
}) builder.addCase(changeShowRead.pending, (state, action) => {
builder.addCase(changeMobileFooter.pending, (state, action) => { if (!state.settings) return
if (!state.settings) return state.settings.showRead = action.meta.arg
state.settings.mobileFooter = action.meta.arg })
}) builder.addCase(changeScrollMarks.pending, (state, action) => {
builder.addCase(changeSharingSetting.pending, (state, action) => { if (!state.settings) return
if (!state.settings) return state.settings.scrollMarks = action.meta.arg
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value })
}) builder.addCase(changeScrollMode.pending, (state, action) => {
builder.addMatcher( if (!state.settings) return
isAnyOf( state.settings.scrollMode = action.meta.arg
changeLanguage.fulfilled, })
changeScrollSpeed.fulfilled, builder.addCase(changeEntriesToKeepOnTopWhenScrolling.pending, (state, action) => {
changeShowRead.fulfilled, if (!state.settings) return
changeScrollMarks.fulfilled, state.settings.entriesToKeepOnTopWhenScrolling = action.meta.arg
changeScrollMode.fulfilled, })
changeStarIconDisplayMode.fulfilled, builder.addCase(changeStarIconDisplayMode.pending, (state, action) => {
changeExternalLinkIconDisplayMode.fulfilled, if (!state.settings) return
changeMarkAllAsReadConfirmation.fulfilled, state.settings.starIconDisplayMode = action.meta.arg
changeCustomContextMenu.fulfilled, })
changeMobileFooter.fulfilled, builder.addCase(changeExternalLinkIconDisplayMode.pending, (state, action) => {
changeSharingSetting.fulfilled if (!state.settings) return
), state.settings.externalLinkIconDisplayMode = action.meta.arg
() => { })
showNotification({ builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
message: t`Settings saved.`, if (!state.settings) return
color: "green", state.settings.markAllAsReadConfirmation = action.meta.arg
}) })
} builder.addCase(changeCustomContextMenu.pending, (state, action) => {
) if (!state.settings) return
}, state.settings.customContextMenu = action.meta.arg
}) })
builder.addCase(changeMobileFooter.pending, (state, action) => {
if (!state.settings) return
state.settings.mobileFooter = action.meta.arg
})
builder.addCase(changeUnreadCountTitle.pending, (state, action) => {
if (!state.settings) return
state.settings.unreadCountTitle = action.meta.arg
})
builder.addCase(changeUnreadCountFavicon.pending, (state, action) => {
if (!state.settings) return
state.settings.unreadCountFavicon = action.meta.arg
})
builder.addCase(changePrimaryColor.pending, (state, action) => {
if (!state.settings) return
state.settings.primaryColor = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
})
builder.addMatcher(
isAnyOf(
changeLanguage.fulfilled,
changeScrollSpeed.fulfilled,
changeShowRead.fulfilled,
changeScrollMarks.fulfilled,
changeScrollMode.fulfilled,
changeEntriesToKeepOnTopWhenScrolling.fulfilled,
changeStarIconDisplayMode.fulfilled,
changeExternalLinkIconDisplayMode.fulfilled,
changeMarkAllAsReadConfirmation.fulfilled,
changeCustomContextMenu.fulfilled,
changeMobileFooter.fulfilled,
changeUnreadCountTitle.fulfilled,
changeUnreadCountFavicon.fulfilled,
changePrimaryColor.fulfilled,
changeSharingSetting.fulfilled
),
() => {
showNotification({
message: t`Settings saved.`,
color: "green",
})
}
)
},
})
export const { setViewMode, setSidebarWidth, setAnnouncementHash, setFontSizePercentage } = userSlice.actions

View File

@@ -1,99 +1,141 @@
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))
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data)) export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
const { settings } = thunkApi.getState().user export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
if (!settings) return
client.user.saveSettings({ ...settings, readingMode }) export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
thunkApi.dispatch(reloadEntries()) const { settings } = thunkApi.getState().user
}) if (!settings) return
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => { client.user.saveSettings({ ...settings, readingMode })
const { settings } = thunkApi.getState().user thunkApi.dispatch(reloadEntries())
if (!settings) return })
client.user.saveSettings({ ...settings, readingOrder })
thunkApi.dispatch(reloadEntries()) export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
}) const { settings } = thunkApi.getState().user
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => { if (!settings) return
const { settings } = thunkApi.getState().user client.user.saveSettings({ ...settings, readingOrder })
if (!settings) return thunkApi.dispatch(reloadEntries())
client.user.saveSettings({ ...settings, language }) })
})
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => { export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
const { settings } = thunkApi.getState().user const { settings } = thunkApi.getState().user
if (!settings) return if (!settings) return
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 }) client.user.saveSettings({ ...settings, language })
}) })
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
if (!settings) return const { settings } = thunkApi.getState().user
client.user.saveSettings({ ...settings, showRead }) if (!settings) return
}) client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => { })
const { settings } = thunkApi.getState().user
if (!settings) return export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
client.user.saveSettings({ ...settings, scrollMarks }) const { settings } = thunkApi.getState().user
}) if (!settings) return
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => { client.user.saveSettings({ ...settings, showRead })
const { settings } = thunkApi.getState().user })
if (!settings) return
client.user.saveSettings({ ...settings, scrollMode }) export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
}) const { settings } = thunkApi.getState().user
export const changeStarIconDisplayMode = createAppAsyncThunk( if (!settings) return
"settings/starIconDisplayMode", client.user.saveSettings({ ...settings, scrollMarks })
(starIconDisplayMode: IconDisplayMode, thunkApi) => { })
const { settings } = thunkApi.getState().user
if (!settings) return export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
client.user.saveSettings({ ...settings, starIconDisplayMode }) const { settings } = thunkApi.getState().user
} if (!settings) return
) client.user.saveSettings({ ...settings, scrollMode })
export const changeExternalLinkIconDisplayMode = createAppAsyncThunk( })
"settings/externalLinkIconDisplayMode",
(externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => { export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
const { settings } = thunkApi.getState().user "settings/entriesToKeepOnTopWhenScrolling",
if (!settings) return (entriesToKeepOnTopWhenScrolling: number, thunkApi) => {
client.user.saveSettings({ ...settings, externalLinkIconDisplayMode }) const { settings } = thunkApi.getState().user
} if (!settings) return
) client.user.saveSettings({ ...settings, entriesToKeepOnTopWhenScrolling })
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk( }
"settings/markAllAsReadConfirmation", )
(markAllAsReadConfirmation: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user export const changeStarIconDisplayMode = createAppAsyncThunk(
if (!settings) return "settings/starIconDisplayMode",
client.user.saveSettings({ ...settings, markAllAsReadConfirmation }) (starIconDisplayMode: IconDisplayMode, thunkApi) => {
} const { settings } = thunkApi.getState().user
) if (!settings) return
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => { client.user.saveSettings({ ...settings, starIconDisplayMode })
const { settings } = thunkApi.getState().user }
if (!settings) return )
client.user.saveSettings({ ...settings, customContextMenu })
}) export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => { "settings/externalLinkIconDisplayMode",
const { settings } = thunkApi.getState().user (externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => {
if (!settings) return const { settings } = thunkApi.getState().user
client.user.saveSettings({ ...settings, mobileFooter }) if (!settings) return
}) client.user.saveSettings({ ...settings, externalLinkIconDisplayMode })
export const changeSharingSetting = createAppAsyncThunk( }
"settings/sharingSetting", )
(
sharingSetting: { export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
site: keyof SharingSettings "settings/markAllAsReadConfirmation",
value: boolean (markAllAsReadConfirmation: boolean, thunkApi) => {
}, const { settings } = thunkApi.getState().user
thunkApi if (!settings) return
) => { client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
const { settings } = thunkApi.getState().user }
if (!settings) return )
client.user.saveSettings({
...settings, export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
sharingSettings: { const { settings } = thunkApi.getState().user
...settings.sharingSettings, if (!settings) return
[sharingSetting.site]: sharingSetting.value, client.user.saveSettings({ ...settings, customContextMenu })
}, })
})
} export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => {
) const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, mobileFooter })
})
export const changeUnreadCountTitle = createAppAsyncThunk("settings/unreadCountTitle", (unreadCountTitle: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, unreadCountTitle })
})
export const changeUnreadCountFavicon = createAppAsyncThunk("settings/unreadCountFavicon", (unreadCountFavicon: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, unreadCountFavicon })
})
export const changePrimaryColor = createAppAsyncThunk("settings/primaryColor", (primaryColor: string, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, primaryColor })
})
export const changeSharingSetting = createAppAsyncThunk(
"settings/sharingSetting",
(
sharingSetting: {
site: keyof SharingSettings
value: boolean
},
thunkApi
) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
}
)

View File

@@ -1,47 +1,69 @@
import { throttle } from "throttle-debounce" import type { TreeCategory } from "app/tree/slice"
import { type Category } from "./types" import { throttle } from "throttle-debounce"
import type { Category } from "./types"
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
visitor(category) export function visitCategoryTree(
category.children.forEach(child => visitCategoryTree(child, visitor)) category: TreeCategory,
} visitor: (category: TreeCategory) => void,
options?: {
export function flattenCategoryTree(category: Category): Category[] { childrenFirst?: boolean
const categories: Category[] = [] }
visitCategoryTree(category, c => categories.push(c)) ): void {
return categories const childrenFirst = options?.childrenFirst
}
if (!childrenFirst) visitor(category)
export function categoryUnreadCount(category?: Category): number {
if (!category) return 0 for (const child of category.children) {
visitCategoryTree(child, visitor, options)
return flattenCategoryTree(category) }
.flatMap(c => c.feeds)
.map(f => f.unread) if (childrenFirst) visitor(category)
.reduce((total, current) => total + current, 0) }
}
export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => { const categories: Category[] = []
const placeholderWidth = width && Math.min(width, maxWidth) visitCategoryTree(category, c => categories.push(c))
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height return categories
return { width: placeholderWidth, height: placeholderHeight } }
}
export function categoryUnreadCount(category?: TreeCategory): number {
export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => { if (!category) return 0
const offset = (options.top ?? 0).toFixed()
return flattenCategoryTree(category)
const onScroll = throttle(100, () => { .flatMap(c => c.feeds)
if (window.scrollY.toFixed() === offset) { .map(f => f.unread)
window.removeEventListener("scroll", onScroll) .reduce((total, current) => total + current, 0)
onScrollEnded() }
}
}) export function categoryHasNewEntries(category?: TreeCategory): boolean {
window.addEventListener("scroll", onScroll) if (!category) return false
// scrollTo does not trigger if there's nothing to do, trigger it manually return flattenCategoryTree(category)
onScroll() .flatMap(c => c.feeds)
.some(f => f.hasNewEntries)
window.scrollTo(options) }
}
export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => {
export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str) const placeholderWidth = width && Math.min(width, maxWidth)
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height
return { width: placeholderWidth, height: placeholderHeight }
}
export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => {
const offset = (options.top ?? 0).toFixed()
const onScroll = throttle(100, () => {
if (window.scrollY.toFixed() === offset) {
window.removeEventListener("scroll", onScroll)
onScrollEnded()
}
})
window.addEventListener("scroll", onScroll)
// scrollTo does not trigger if there's nothing to do, trigger it manually
onScroll()
window.scrollTo(options)
}
export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str)

View File

@@ -0,0 +1,47 @@
import type { I18nContext } from "@lingui/react"
import { MantineProvider } from "@mantine/core"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { useActionButton } from "hooks/useActionButton"
import { describe, expect, it, vi } from "vitest"
import { ActionButton } from "./ActionButton"
vi.mock(import("@lingui/react"), () => ({
useLingui: vi.fn().mockReturnValue({
_: msg => msg,
} as I18nContext),
}))
vi.mock(import("hooks/useActionButton"))
const label = "Test Label"
const icon = "Test Icon"
describe("ActionButton", () => {
it("renders Button with label on desktop", () => {
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 })
render(<ActionButton label={label} icon={icon} />, { wrapper: MantineProvider })
expect(screen.getByText(label)).toBeInTheDocument()
expect(screen.getByText(icon)).toBeInTheDocument()
})
it("renders ActionIcon with tooltip on mobile", async () => {
vi.mocked(useActionButton).mockReturnValue({ mobile: true, spacing: 0 })
render(<ActionButton label={label} icon={icon} />, { wrapper: MantineProvider })
expect(screen.queryByText(label)).not.toBeInTheDocument()
expect(screen.getByText(icon)).toBeInTheDocument()
fireEvent.mouseEnter(screen.getByRole("button"))
const tooltip = await waitFor(() => screen.getByRole("tooltip"))
expect(tooltip).toContainHTML(label)
})
it("calls onClick handler when clicked", () => {
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 })
const clickListener = vi.fn()
render(<ActionButton label={label} icon={icon} onClick={clickListener} />, { wrapper: MantineProvider })
fireEvent.click(screen.getByRole("button"))
expect(clickListener).toHaveBeenCalled()
})
})

View File

@@ -1,37 +1,62 @@
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core" import type { MessageDescriptor } from "@lingui/core"
import { type ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon" import { useLingui } from "@lingui/react"
import { Constants } from "app/constants" import { ActionIcon, Box, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
import { useActionButton } from "hooks/useActionButton" import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
import { forwardRef, type MouseEventHandler, type ReactNode } from "react" import { Constants } from "app/constants"
import { useActionButton } from "hooks/useActionButton"
interface ActionButtonProps { import { type MouseEventHandler, type ReactNode, forwardRef } from "react"
className?: string
icon?: ReactNode interface ActionButtonProps {
label: ReactNode icon: ReactNode
onClick?: MouseEventHandler className?: string
variant?: ActionIconVariant & ButtonVariant label?: string | MessageDescriptor
hideLabelOnDesktop?: boolean onClick?: MouseEventHandler
showLabelOnMobile?: boolean variant?: ActionIconVariant & ButtonVariant
} hideLabelOnDesktop?: boolean
showLabelOnMobile?: boolean
/** }
* Switches between Button with label (desktop) and ActionIcon (mobile)
*/ /**
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => { * Switches between Button with label (desktop) and ActionIcon (mobile)
const { mobile } = useActionButton() */
const theme = useMantineTheme() export const ActionButton = forwardRef<HTMLDivElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
const variant = props.variant ?? "subtle" const { mobile } = useActionButton()
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop) const theme = useMantineTheme()
return iconOnly ? ( const { _ } = useLingui()
<Tooltip label={props.label} openDelay={Constants.tooltip.delay}>
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}> const label = typeof props.label === "string" ? props.label : props.label && _(props.label)
{props.icon} const variant = props.variant ?? "subtle"
</ActionIcon> const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
</Tooltip>
) : ( return (
<Button ref={ref} variant={variant} size="xs" className={props.className} leftSection={props.icon} onClick={props.onClick}> <Box ref={ref} className="cf-action-button">
{props.label} {iconOnly && (
</Button> <Tooltip label={label} openDelay={Constants.tooltip.delay}>
) <ActionIcon
}) color={theme.primaryColor}
ActionButton.displayName = "HeaderButton" variant={variant}
className={props.className}
onClick={props.onClick}
aria-label={label}
>
{props.icon}
</ActionIcon>
</Tooltip>
)}
{!iconOnly && (
<Button
variant={variant}
size="xs"
className={props.className}
leftSection={props.icon}
onClick={props.onClick}
aria-label={label}
>
{label}
</Button>
)}
</Box>
)
})
ActionButton.displayName = "HeaderButton"

View File

@@ -1,47 +1,47 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/react/macro"
import { Alert as MantineAlert, Box } from "@mantine/core" import { Box, Alert as MantineAlert } from "@mantine/core"
import { Fragment } from "react" import { Fragment } from "react"
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb" import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
type Level = "error" | "warning" | "success" type Level = "error" | "warning" | "success"
export interface ErrorsAlertProps { export interface ErrorsAlertProps {
level?: Level level?: Level
messages: string[] messages: string[]
} }
export function Alert(props: ErrorsAlertProps) { export function Alert(props: ErrorsAlertProps) {
let title: React.ReactNode let title: React.ReactNode
let color: string let color: string
let icon: React.ReactNode let icon: React.ReactNode
const level = props.level ?? "error" const level = props.level ?? "error"
switch (level) { switch (level) {
case "error": case "error":
title = <Trans>Error</Trans> title = <Trans>Error</Trans>
color = "red" color = "red"
icon = <TbAlertCircle /> icon = <TbAlertCircle />
break break
case "warning": case "warning":
title = <Trans>Warning</Trans> title = <Trans>Warning</Trans>
color = "orange" color = "orange"
icon = <TbAlertTriangle /> icon = <TbAlertTriangle />
break break
case "success": case "success":
title = <Trans>Success</Trans> title = <Trans>Success</Trans>
color = "green" color = "green"
icon = <TbCircleCheck /> icon = <TbCircleCheck />
break break
} }
return ( return (
<MantineAlert title={title} color={color} icon={icon}> <MantineAlert title={title} color={color} icon={icon}>
{props.messages.map((m, i) => ( {props.messages.map((m, i) => (
<Fragment key={m}> <Fragment key={m}>
<Box>{m}</Box> <Box>{m}</Box>
{i !== props.messages.length - 1 && <br />} {i !== props.messages.length - 1 && <br />}
</Fragment> </Fragment>
))} ))}
</MantineAlert> </MantineAlert>
) )
} }

View File

@@ -1,9 +1,9 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/react/macro"
import { Box, Dialog, Text } from "@mantine/core" import { Box, Dialog, Text } from "@mantine/core"
import { useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { setAnnouncementHash } from "app/user/slice"
import { Content } from "components/content/Content" import { Content } from "components/content/Content"
import { useAsync } from "react-async-hook" import { useAsync } from "react-async-hook"
import useLocalStorage from "use-local-storage"
const sha256Hex = async (input: string | undefined) => { const sha256Hex = async (input: string | undefined) => {
const data = new TextEncoder().encode(input) const data = new TextEncoder().encode(input)
@@ -15,10 +15,11 @@ const sha256Hex = async (input: string | undefined) => {
export function AnnouncementDialog() { export function AnnouncementDialog() {
const announcement = useAppSelector(state => state.server.serverInfos?.announcement) const announcement = useAppSelector(state => state.server.serverInfos?.announcement)
const announcementHash = useAsync(sha256Hex, [announcement]).result const announcementHash = useAsync(sha256Hex, [announcement]).result
const [localStorageHash, setLocalStorageHash] = useLocalStorage("announcement-hash", "no-hash") const existingAnnouncementHash = useAppSelector(state => state.user.localSettings.announcementHash)
const dispatch = useAppDispatch()
const opened = !!announcementHash && announcementHash !== localStorageHash const opened = !!announcementHash && announcementHash !== existingAnnouncementHash
const onClosed = () => setLocalStorageHash(announcementHash) const onClosed = () => announcementHash && dispatch(setAnnouncementHash(announcementHash))
if (!announcement) return null if (!announcement) return null
return ( return (

View File

@@ -0,0 +1,4 @@
html,
body {
overscroll-behavior: none;
}

View File

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

View File

@@ -1,26 +1,26 @@
import { ErrorPage } from "pages/ErrorPage" import { ErrorPage } from "pages/ErrorPage"
import React, { type ReactNode } from "react" import React, { type ReactNode } from "react"
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
children?: ReactNode children?: ReactNode
} }
interface ErrorBoundaryState { interface ErrorBoundaryState {
error?: Error error?: Error
} }
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) { constructor(props: ErrorBoundaryProps) {
super(props) super(props)
this.state = {} this.state = {}
} }
componentDidCatch(error: Error) { componentDidCatch(error: Error) {
this.setState({ error }) this.setState({ error })
} }
render() { render() {
if (this.state.error) return <ErrorPage error={this.state.error} /> if (this.state.error) return <ErrorPage error={this.state.error} />
return this.props.children return this.props.children
} }
} }

View File

@@ -1,75 +1,81 @@
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
alt: string alt: string
title?: string title?: string
width?: number width?: number
height?: number | "auto" height?: number | "auto"
placeholderWidth?: number style?: React.CSSProperties
placeholderHeight?: number placeholderWidth?: number
placeholderBackgroundColor?: string placeholderHeight?: number
placeholderIconSize?: number placeholderBackgroundColor?: string
} placeholderIconSize?: number
}
const useStyles = tss
.withParams<{ const useStyles = tss
placeholderWidth?: number .withParams<{
placeholderHeight?: number placeholderWidth?: number
placeholderBackgroundColor?: string placeholderHeight?: number
}>() placeholderBackgroundColor?: string
.create(props => ({ }>()
placeholder: { .create(props => ({
width: props.placeholderWidth ?? 400, placeholder: {
height: props.placeholderHeight ?? 600, width: props.placeholderWidth ?? 400,
maxWidth: "100%", height: props.placeholderHeight ?? 600,
backgroundColor: maxWidth: "100%",
props.placeholderBackgroundColor ?? backgroundColor:
(props.colorScheme === "dark" ? props.theme.colors.dark[5] : props.theme.colors.gray[1]), props.placeholderBackgroundColor ??
}, (props.colorScheme === "dark" ? props.theme.colors.dark[5] : props.theme.colors.gray[1]),
})) },
}))
export function ImageWithPlaceholderWhileLoading({
alt, export function ImageWithPlaceholderWhileLoading({
height, alt,
placeholderBackgroundColor, height,
placeholderHeight, placeholderBackgroundColor,
placeholderIconSize, placeholderHeight,
placeholderWidth, placeholderIconSize,
src, placeholderWidth,
title, src,
width, title,
}: ImageWithPlaceholderWhileLoadingProps) { width,
const { classes } = useStyles({ style,
placeholderWidth, }: ImageWithPlaceholderWhileLoadingProps) {
placeholderHeight, const { classes } = useStyles({
placeholderBackgroundColor, placeholderWidth,
}) placeholderHeight,
const [loading, setLoading] = useState(true) placeholderBackgroundColor,
})
return ( const [loading, setLoading] = useState(true)
<>
{loading && ( return (
<Box> <>
<Center className={classes.placeholder}> {loading && (
<div> <Box>
<TbPhoto size={placeholderIconSize ?? 48} /> <Center className={classes.placeholder}>
</div> <div>
</Center> <TbPhoto size={placeholderIconSize ?? 48} />
</Box> </div>
)} </Center>
<img </Box>
src={src} )}
alt={alt} <img
title={title} src={src}
width={width} alt={alt}
height={height} title={title}
onLoad={() => setLoading(false)} width={width}
style={{ display: loading ? "none" : "block" }} height={height}
/> onLoad={() => setLoading(false)}
</> style={{
) ...style,
} display: loading ? "none" : (style?.display ?? "initial"),
height: style?.width ? "auto" : style?.height,
}}
/>
</>
)
}

View File

@@ -1,222 +1,242 @@
import { Trans } from "@lingui/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 { Constants } from "app/constants" import { useOs } from "@mantine/hooks"
import { Constants } from "app/constants"
export function KeyboardShortcutsHelp() {
return ( export function KeyboardShortcutsHelp() {
<Stack gap="xs"> const isMacOS = useOs() === "macos"
<Table striped highlightOnHover> return (
<Table.Tbody> <Stack gap="xs">
<Table.Tr> <Table striped highlightOnHover>
<Table.Td> <Table.Tbody>
<Trans>Refresh</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Refresh</Trans>
<Kbd>R</Kbd> </Table.Td>
</Table.Td> <Table.Td>
</Table.Tr> <Kbd>R</Kbd>
<Table.Tr> </Table.Td>
<Table.Td> </Table.Tr>
<Trans>Open next entry</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Open next entry</Trans>
<Kbd>J</Kbd> </Table.Td>
</Table.Td> <Table.Td>
</Table.Tr> <Kbd>J</Kbd>
<Table.Tr> </Table.Td>
<Table.Td> </Table.Tr>
<Trans>Open previous entry</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Open previous entry</Trans>
<Kbd>K</Kbd> </Table.Td>
</Table.Td> <Table.Td>
</Table.Tr> <Kbd>K</Kbd>
<Table.Tr> </Table.Td>
<Table.Td> </Table.Tr>
<Trans>Set focus on next entry without opening it</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Select next unread feed/category</Trans>
<Kbd>N</Kbd> </Table.Td>
</Table.Td> <Table.Td>
</Table.Tr> <Kbd>Shift</Kbd>
<Table.Tr> <span> + </span>
<Table.Td> <Kbd>J</Kbd>
<Trans>Set focus on previous entry without opening it</Trans> </Table.Td>
</Table.Td> </Table.Tr>
<Table.Td> <Table.Tr>
<Kbd>P</Kbd> <Table.Td>
</Table.Td> <Trans>Select previous unread feed/category</Trans>
</Table.Tr> </Table.Td>
<Table.Tr> <Table.Td>
<Table.Td> <Kbd>Shift</Kbd>
<Trans>Move the page down</Trans> <span> + </span>
</Table.Td> <Kbd>K</Kbd>
<Table.Td> </Table.Td>
<Kbd> </Table.Tr>
<Trans>Space</Trans> <Table.Tr>
</Kbd> <Table.Td>
</Table.Td> <Trans>Set focus on next entry without opening it</Trans>
</Table.Tr> </Table.Td>
<Table.Tr> <Table.Td>
<Table.Td> <Kbd>N</Kbd>
<Trans>Move the page up</Trans> </Table.Td>
</Table.Td> </Table.Tr>
<Table.Td> <Table.Tr>
<Kbd> <Table.Td>
<Trans>Shift</Trans> <Trans>Set focus on previous entry without opening it</Trans>
</Kbd> </Table.Td>
<span> + </span> <Table.Td>
<Kbd> <Kbd>P</Kbd>
<Trans>Space</Trans> </Table.Td>
</Kbd> </Table.Tr>
</Table.Td> <Table.Tr>
</Table.Tr> <Table.Td>
<Table.Tr> <Trans>Move the page down</Trans>
<Table.Td> </Table.Td>
<Trans>Open/close current entry</Trans> <Table.Td>
</Table.Td> <Kbd>
<Table.Td> <Trans>Space</Trans>
<Kbd>O</Kbd> </Kbd>
<span>, </span> </Table.Td>
<Kbd> </Table.Tr>
<Trans>Enter</Trans> <Table.Tr>
</Kbd> <Table.Td>
</Table.Td> <Trans>Move the page up</Trans>
</Table.Tr> </Table.Td>
<Table.Tr> <Table.Td>
<Table.Td> <Kbd>
<Trans>Open current entry in a new tab</Trans> <Trans>Shift</Trans>
</Table.Td> </Kbd>
<Table.Td> <span> + </span>
<Kbd>V</Kbd> <Kbd>
</Table.Td> <Trans>Space</Trans>
</Table.Tr> </Kbd>
<Table.Tr> </Table.Td>
<Table.Td> </Table.Tr>
<Trans>Open current entry in a new tab in the background</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Open/close current entry</Trans>
<Kbd>B</Kbd> </Table.Td>
<span>*, </span> <Table.Td>
<Kbd> <Kbd>O</Kbd>
<Trans>Middle click</Trans> <span>, </span>
</Kbd> <Kbd>
</Table.Td> <Trans>Enter</Trans>
</Table.Tr> </Kbd>
<Table.Tr> </Table.Td>
<Table.Td> </Table.Tr>
<Trans>Toggle read status of current entry</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Open current entry in a new tab</Trans>
<Kbd>M</Kbd> </Table.Td>
<span>, </span> <Table.Td>
<Trans>Swipe header to the left</Trans> <Kbd>V</Kbd>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
<Table.Tr> <Table.Tr>
<Table.Td> <Table.Td>
<Trans>Toggle starred status of current entry</Trans> <Trans>Open current entry in a new tab in the background</Trans>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Kbd>S</Kbd> <Kbd>B</Kbd>
</Table.Td> <span>*, </span>
</Table.Tr> <Kbd>
<Table.Tr> <Trans>Middle click</Trans>
<Table.Td> </Kbd>
<Trans>Mark all entries as read</Trans> </Table.Td>
</Table.Td> </Table.Tr>
<Table.Td> <Table.Tr>
<Kbd> <Table.Td>
<Trans>Shift</Trans> <Trans>Toggle read status of current entry</Trans>
</Kbd> </Table.Td>
<span> + </span> <Table.Td>
<Kbd>A</Kbd> <Kbd>M</Kbd>
</Table.Td> <span>, </span>
</Table.Tr> <Trans>Swipe header to the left</Trans>
<Table.Tr> </Table.Td>
<Table.Td> </Table.Tr>
<Trans>Go to the All view</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Toggle starred status of current entry</Trans>
<Kbd>G</Kbd> </Table.Td>
<span> </span> <Table.Td>
<Kbd>A</Kbd> <Kbd>S</Kbd>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
<Table.Tr> <Table.Tr>
<Table.Td> <Table.Td>
<Trans>Navigate to a subscription by entering its name</Trans> <Trans>Mark all entries as read</Trans>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Kbd> <Kbd>
<Trans>Ctrl</Trans> <Trans>Shift</Trans>
</Kbd> </Kbd>
<span> + </span> <span> + </span>
<Kbd>K</Kbd> <Kbd>A</Kbd>
<span>, </span> </Table.Td>
<Kbd>G</Kbd> </Table.Tr>
<span> </span> <Table.Tr>
<Kbd>U</Kbd> <Table.Td>
</Table.Td> <Trans>Go to the All view</Trans>
</Table.Tr> </Table.Td>
<Table.Tr> <Table.Td>
<Table.Td> <Kbd>G</Kbd>
<Trans>Show entry menu (desktop)</Trans> <span> </span>
</Table.Td> <Kbd>A</Kbd>
<Table.Td> </Table.Td>
<Kbd> </Table.Tr>
<Trans>Right click</Trans> <Table.Tr>
</Kbd> <Table.Td>
</Table.Td> <Trans>Navigate to a subscription by entering its name</Trans>
</Table.Tr> </Table.Td>
<Table.Tr> <Table.Td>
<Table.Td> <Kbd>{isMacOS ? <Trans>Cmd</Trans> : <Trans>Ctrl</Trans>}</Kbd>
<Trans>Show native menu (desktop)</Trans> <span> + </span>
</Table.Td> <Kbd>K</Kbd>
<Table.Td> <span>, </span>
<Kbd> <Kbd>G</Kbd>
<Trans>Shift</Trans> <span> </span>
</Kbd> <Kbd>U</Kbd>
<span> + </span> </Table.Td>
<Kbd> </Table.Tr>
<Trans>Right click</Trans> <Table.Tr>
</Kbd> <Table.Td>
</Table.Td> <Trans>Show entry menu (desktop)</Trans>
</Table.Tr> </Table.Td>
<Table.Tr> <Table.Td>
<Table.Td> <Kbd>
<Trans>Show entry menu (mobile)</Trans> <Trans>Right click</Trans>
</Table.Td> </Kbd>
<Table.Td> </Table.Td>
<Kbd> </Table.Tr>
<Trans>Long press</Trans> <Table.Tr>
</Kbd> <Table.Td>
</Table.Td> <Trans>Show native menu (desktop)</Trans>
</Table.Tr> </Table.Td>
<Table.Tr> <Table.Td>
<Table.Td> <Kbd>
<Trans>Toggle sidebar</Trans> <Trans>Shift</Trans>
</Table.Td> </Kbd>
<Table.Td> <span> + </span>
<Kbd>F</Kbd> <Kbd>
</Table.Td> <Trans>Right click</Trans>
</Table.Tr> </Kbd>
<Table.Tr> </Table.Td>
<Table.Td> </Table.Tr>
<Trans>Show keyboard shortcut help</Trans> <Table.Tr>
</Table.Td> <Table.Td>
<Table.Td> <Trans>Show entry menu (mobile)</Trans>
<Kbd>?</Kbd> </Table.Td>
</Table.Td> <Table.Td>
</Table.Tr> <Kbd>
</Table.Tbody> <Trans>Long press</Trans>
</Table> </Kbd>
<Box> </Table.Td>
<span>* </span> </Table.Tr>
<Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer"> <Table.Tr>
<Trans>Browser extension required for Chrome</Trans> <Table.Td>
</Anchor> <Trans>Toggle sidebar</Trans>
</Box> </Table.Td>
</Stack> <Table.Td>
) <Kbd>F</Kbd>
} </Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Show keyboard shortcut help</Trans>
</Table.Td>
<Table.Td>
<Kbd>?</Kbd>
</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
<Box>
<span>* </span>
<Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
<Trans>Browser extension required for Chrome</Trans>
</Anchor>
</Box>
</Stack>
)
}

View File

@@ -1,9 +1,9 @@
import { Center, Loader as MantineLoader } from "@mantine/core" import { Center, Loader as MantineLoader } from "@mantine/core"
export function Loader() { export function Loader() {
return ( return (
<Center> <Center>
<MantineLoader size="lg" type="bars" /> <MantineLoader size="lg" type="bars" />
</Center> </Center>
) )
} }

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: LogoProps) {
return <Image src={logo} w={props.size} /> return <Image src={logo} w={props.size} />
} }

View File

@@ -0,0 +1,77 @@
import { Trans } from "@lingui/react/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { setMarkAllAsReadConfirmationDialogOpen } from "app/entries/slice"
import { markAllEntries } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { useState } from "react"
export function MarkAllAsReadConfirmationDialog() {
const [threshold, setThreshold] = useState(0)
const open = useAppSelector(state => state.entries.markAllAsReadConfirmationDialogOpen)
const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const dispatch = useAppDispatch()
const onConfirm = () => {
dispatch(
markAllEntries({
sourceType: source.type,
req: {
id: source.id,
read: true,
olderThan: Date.now() - threshold * 24 * 60 * 60 * 1000,
insertedBefore: entriesTimestamp,
},
})
)
dispatch(setMarkAllAsReadConfirmationDialogOpen(false))
}
return (
<Modal
opened={open}
onClose={() => dispatch(setMarkAllAsReadConfirmationDialogOpen(false))}
title={<Trans>Mark all entries as read</Trans>}
>
<Stack>
<Text size="sm">
{threshold === 0 && (
<Trans>
Are you sure you want to mark all entries of <Code>{sourceLabel}</Code> as read?
</Trans>
)}
{threshold > 0 && (
<Trans>
Are you sure you want to mark entries older than {threshold} days of <Code>{sourceLabel}</Code> as read?
</Trans>
)}
</Text>
<Slider
py="xl"
min={0}
max={28}
marks={[
{ value: 0, label: "0" },
{ value: 7, label: "7" },
{ value: 14, label: "14" },
{ value: 21, label: "21" },
{ value: 28, label: "28" },
]}
value={threshold}
onChange={setThreshold}
data-autofocus
onKeyDown={e => e.key === "Enter" && onConfirm()}
/>
<Group justify="flex-end">
<Button variant="default" onClick={() => dispatch(setMarkAllAsReadConfirmationDialogOpen(false))}>
<Trans>Cancel</Trans>
</Button>
<Button color="red" onClick={onConfirm}>
<Trans>Confirm</Trans>
</Button>
</Group>
</Stack>
</Modal>
)
}

View File

@@ -1,21 +1,17 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/react/macro"
import { Tooltip } from "@mantine/core" import { Tooltip } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import dayjs from "dayjs" import dayjs from "dayjs"
import { useEffect, useState } from "react" import { useNow } from "hooks/useNow"
export function RelativeDate(props: { date: Date | number | undefined }) { export function RelativeDate(props: { date: Date | number | undefined }) {
const [now, setNow] = useState(new Date()) const now = useNow(60 * 1000)
useEffect(() => {
const interval = setInterval(() => setNow(new Date()), 60 * 1000) if (!props.date) return <Trans>N/A</Trans>
return () => clearInterval(interval) const date = dayjs(props.date)
}, []) return (
<Tooltip label={date.toDate().toLocaleString()} openDelay={Constants.tooltip.delay}>
if (!props.date) return <Trans>N/A</Trans> <span>{date.from(dayjs(now))}</span>
const date = dayjs(props.date) </Tooltip>
return ( )
<Tooltip label={date.toDate().toLocaleString()} openDelay={Constants.tooltip.delay}> }
<span>{date.from(dayjs(now))}</span>
</Tooltip>
)
}

View File

@@ -1,54 +1,54 @@
import { Trans } from "@lingui/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 { client, errorToStrings } from "app/client"
import { type AdminSaveUserRequest, type UserModel } from "app/types" import type { AdminSaveUserRequest, UserModel } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb" import { TbDeviceFloppy } from "react-icons/tb"
interface UserEditProps { interface UserEditProps {
user?: UserModel user?: UserModel
onCancel: () => void onCancel: () => void
onSave: () => void onSave: () => void
} }
export function UserEdit(props: UserEditProps) { export function UserEdit(props: UserEditProps) {
const form = useForm<AdminSaveUserRequest>({ const form = useForm<AdminSaveUserRequest>({
initialValues: props.user ?? { initialValues: props.user ?? {
name: "", name: "",
enabled: true, enabled: true,
admin: false, admin: false,
}, },
}) })
const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave }) const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave })
return ( return (
<> <>
{saveUser.error && ( {saveUser.error && (
<Box mb="md"> <Box mb="md">
<Alert messages={errorToStrings(saveUser.error)} /> <Alert messages={errorToStrings(saveUser.error)} />
</Box> </Box>
)} )}
<form onSubmit={form.onSubmit(saveUser.execute)}> <form onSubmit={form.onSubmit(saveUser.execute)}>
<Stack> <Stack>
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required /> <TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
<PasswordInput label={<Trans>Password</Trans>} {...form.getInputProps("password")} required={!props.user} /> <PasswordInput label={<Trans>Password</Trans>} {...form.getInputProps("password")} required={!props.user} />
<TextInput type="email" label={<Trans>E-mail</Trans>} {...form.getInputProps("email")} /> <TextInput type="email" label={<Trans>E-mail</Trans>} {...form.getInputProps("email")} />
<Checkbox label={<Trans>Admin</Trans>} {...form.getInputProps("admin", { type: "checkbox" })} /> <Checkbox label={<Trans>Admin</Trans>} {...form.getInputProps("admin", { type: "checkbox" })} />
<Checkbox label={<Trans>Enabled</Trans>} {...form.getInputProps("enabled", { type: "checkbox" })} /> <Checkbox label={<Trans>Enabled</Trans>} {...form.getInputProps("enabled", { type: "checkbox" })} />
<Group justify="right"> <Group justify="right">
<Button variant="default" onClick={props.onCancel}> <Button variant="default" onClick={props.onCancel}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveUser.loading}> <Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveUser.loading}>
<Trans>Save</Trans> <Trans>Save</Trans>
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form> </form>
</> </>
) )
} }

View File

@@ -1,36 +1,38 @@
import { Input, Textarea } from "@mantine/core" import { Input, Textarea } from "@mantine/core"
import RichCodeEditor from "components/code/RichCodeEditor" import RichCodeEditor from "components/code/RichCodeEditor"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
import { type ReactNode } from "react" import type { ReactNode } from "react"
interface CodeEditorProps { interface CodeEditorProps {
description?: ReactNode label?: ReactNode
language: "css" | "javascript" description?: ReactNode
value?: string language: "css" | "javascript"
onChange: (value: string | undefined) => void value?: string
} onChange: (value: string | undefined) => void
}
export function CodeEditor(props: CodeEditorProps) {
const mobile = useMobile() export function CodeEditor(props: CodeEditorProps) {
const mobile = useMobile()
return mobile ? (
// monaco mobile support is poor, fallback to textarea return mobile ? (
<Textarea // monaco mobile support is poor, fallback to textarea
autosize <Textarea
minRows={4} autosize
maxRows={15} minRows={4}
description={props.description} maxRows={15}
styles={{ label={props.label}
input: { description={props.description}
fontFamily: "monospace", styles={{
}, input: {
}} fontFamily: "monospace",
value={props.value} },
onChange={e => props.onChange(e.currentTarget.value)} }}
/> value={props.value}
) : ( onChange={e => props.onChange(e.currentTarget.value)}
<Input.Wrapper description={props.description}> />
<RichCodeEditor height="30vh" language={props.language} value={props.value} onChange={props.onChange} /> ) : (
</Input.Wrapper> <Input.Wrapper label={props.label} description={props.description}>
) <RichCodeEditor height="30vh" language={props.language} value={props.value} onChange={props.onChange} />
} </Input.Wrapper>
)
}

View File

@@ -1,52 +1,51 @@
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useColorScheme } from "hooks/useColorScheme" import { useColorScheme } from "hooks/useColorScheme"
import { useAsync } from "react-async-hook" import { useAsync } from "react-async-hook"
const init = async () => { const init = async () => {
window.MonacoEnvironment = { window.MonacoEnvironment = {
async getWorker(_, label) { async getWorker(_, label) {
let worker let worker: typeof import("*?worker")
if (label === "css") { if (label === "css") {
worker = await import("monaco-editor/esm/vs/language/css/css.worker?worker") worker = await import("monaco-editor/esm/vs/language/css/css.worker?worker")
} else if (label === "javascript") { } else if (label === "javascript") {
worker = await import("monaco-editor/esm/vs/language/typescript/ts.worker?worker") worker = await import("monaco-editor/esm/vs/language/typescript/ts.worker?worker")
} else { } else {
worker = await import("monaco-editor/esm/vs/editor/editor.worker?worker") worker = await import("monaco-editor/esm/vs/editor/editor.worker?worker")
} }
// eslint-disable-next-line new-cap return new worker.default()
return new worker.default() },
}, }
}
const monacoReact = await import("@monaco-editor/react")
const monacoReact = await import("@monaco-editor/react") const monaco = await import("monaco-editor")
const monaco = await import("monaco-editor") monacoReact.loader.config({ monaco })
monacoReact.loader.config({ monaco }) return monacoReact.Editor
return monacoReact.Editor }
}
interface RichCodeEditorProps {
interface RichCodeEditorProps { height: number | string
height: number | string language: "css" | "javascript"
language: "css" | "javascript" value?: string
value?: string onChange: (value: string | undefined) => void
onChange: (value: string | undefined) => void }
}
function RichCodeEditor(props: RichCodeEditorProps) {
function RichCodeEditor(props: RichCodeEditorProps) { const colorScheme = useColorScheme()
const colorScheme = useColorScheme() const editorTheme = colorScheme === "dark" ? "vs-dark" : "light"
const editorTheme = colorScheme === "dark" ? "vs-dark" : "light"
const { result: Editor } = useAsync(init, [])
const { result: Editor } = useAsync(init, []) if (!Editor) return <Loader />
if (!Editor) return <Loader /> return (
return ( <Editor
<Editor height={props.height}
height={props.height} defaultLanguage={props.language}
defaultLanguage={props.language} theme={editorTheme}
theme={editorTheme} options={{ minimap: { enabled: false } }}
options={{ minimap: { enabled: false } }} value={props.value}
value={props.value} onChange={props.onChange}
onChange={props.onChange} />
/> )
) }
}
export default RichCodeEditor
export default RichCodeEditor

View File

@@ -1,11 +1,24 @@
import { TypographyStylesProvider } from "@mantine/core" import { TypographyStylesProvider } from "@mantine/core"
import { type ReactNode } from "react" import type { ReactNode } from "react"
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.
* see https://mantine.dev/core/typography-styles-provider/ *
*/ * see https://mantine.dev/core/typography-styles-provider/
export const BasicHtmlStyles = (props: { children: ReactNode }) => { */
return <TypographyStylesProvider pl={0}>{props.children}</TypographyStylesProvider>
} const useStyles = tss.create(() => ({
// override mantine default typography styles
content: {
paddingLeft: 0,
"& img": {
marginBottom: 0,
},
},
}))
export const BasicHtmlStyles = (props: { children: ReactNode }) => {
const { classes } = useStyles()
return <TypographyStylesProvider className={classes.content}>{props.children}</TypographyStylesProvider>
}

View File

@@ -0,0 +1,26 @@
import { MantineProvider } from "@mantine/core"
import { render } from "@testing-library/react"
import { Content } from "components/content/Content"
import { describe, expect, it } from "vitest"
describe("Content component", () => {
it("renders basic content", () => {
const { container } = render(<Content content="<p>Hello World</p>" />, { wrapper: MantineProvider })
expect(container.querySelector("p")).toHaveTextContent("Hello World")
})
it("renders highlighted text when highlight prop is provided", () => {
const { container } = render(<Content content="Hello World" highlight="World" />, { wrapper: MantineProvider })
expect(container.querySelector("mark")).toHaveTextContent("World")
})
it("renders iframe tag when included in content", () => {
const { container } = render(<Content content='<iframe src="https://example.com"></iframe>' />, { wrapper: MantineProvider })
expect(container.querySelector("iframe")).toHaveAttribute("src", "https://example.com")
})
it("does not render unsupported tags", () => {
const { container } = render(<Content content='<script>alert("test")</script>' />, { wrapper: MantineProvider })
expect(container.querySelector("script")).toBeNull()
})
})

View File

@@ -1,103 +1,108 @@
import { Box, Mark } from "@mantine/core" import { Box, Mark } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { calculatePlaceholderSize } from "app/utils" import { calculatePlaceholderSize } from "app/utils"
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
import escapeStringRegexp from "escape-string-regexp" import escapeStringRegexp from "escape-string-regexp"
import { type ChildrenNode, Interweave, Matcher, type MatchResponse, type Node, type TransformCallback } from "interweave" import { ALLOWED_TAG_LIST, type ChildrenNode, Interweave, type MatchResponse, Matcher, type Node, type TransformCallback } from "interweave"
import React from "react" import React from "react"
import { tss } from "tss" import styleToObject from "style-to-object"
import { tss } from "tss"
export interface ContentProps {
content: string export interface ContentProps {
highlight?: string content: string
} highlight?: string
}
const useStyles = tss.create(() => ({
content: { const useStyles = tss.create(() => ({
// break long links or long words content: {
overflowWrap: "anywhere", // break long links or long words
"& a": { overflowWrap: "anywhere",
color: "inherit", "& a": {
textDecoration: "underline", color: "inherit",
}, textDecoration: "underline",
"& iframe": { },
maxWidth: "100%", "& iframe": {
}, maxWidth: "100%",
"& pre, & code": { },
whiteSpace: "pre-wrap", "& pre, & code": {
}, whiteSpace: "pre-wrap",
}, },
})) },
}))
const transform: TransformCallback = node => {
if (node.tagName === "IMG") { const transform: TransformCallback = node => {
// show placeholders for loading img tags, this allows the entry to have its final height immediately if (node.tagName === "IMG") {
const src = node.getAttribute("src") ?? undefined // show placeholders for loading img tags, this allows the entry to have its final height immediately
if (!src) return undefined const src = node.getAttribute("src") ?? undefined
if (!src) return undefined
const alt = node.getAttribute("alt") ?? "image"
const title = node.getAttribute("title") ?? undefined const alt = node.getAttribute("alt") ?? "image"
const nodeWidth = node.getAttribute("width") const title = node.getAttribute("title") ?? undefined
const nodeHeight = node.getAttribute("height") const nodeWidth = node.getAttribute("width")
const width = nodeWidth ? parseInt(nodeWidth, 10) : undefined const nodeHeight = node.getAttribute("height")
const height = nodeHeight ? parseInt(nodeHeight, 10) : undefined const width = nodeWidth ? Number.parseInt(nodeWidth, 10) : undefined
const placeholderSize = calculatePlaceholderSize({ const height = nodeHeight ? Number.parseInt(nodeHeight, 10) : undefined
width, const style = styleToObject(node.getAttribute("style") ?? "") ?? undefined
height, const placeholderSize = calculatePlaceholderSize({
maxWidth: Constants.layout.entryMaxWidth, width,
}) height,
maxWidth: Constants.layout.entryMaxWidth,
return ( })
<ImageWithPlaceholderWhileLoading
src={src} return (
alt={alt} <ImageWithPlaceholderWhileLoading
title={title} src={src}
width={width} alt={alt}
height="auto" title={title}
placeholderWidth={placeholderSize.width} width={width}
placeholderHeight={placeholderSize.height} height="auto"
/> style={style}
) placeholderWidth={placeholderSize.width}
} placeholderHeight={placeholderSize.height}
return undefined />
} )
}
class HighlightMatcher extends Matcher { return undefined
private readonly search: string }
constructor(search: string) { class HighlightMatcher extends Matcher {
super("highlight") private readonly regexp: RegExp
this.search = escapeStringRegexp(search)
} constructor(search: string) {
super("highlight")
match(string: string): MatchResponse<unknown> | null { this.regexp = new RegExp(escapeStringRegexp(search).split(" ").join("|"), "i")
const pattern = this.search.split(" ").join("|") }
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
} match(string: string): MatchResponse<unknown> | null {
return this.doMatch(string, this.regexp, () => ({}))
replaceWith(children: ChildrenNode): Node { }
return <Mark>{children}</Mark>
} replaceWith(children: ChildrenNode): Node {
return <Mark key={0}>{children}</Mark>
asTag(): string { }
return "span"
} asTag(): string {
} return "span"
}
// memoize component because Interweave is costly }
const Content = React.memo((props: ContentProps) => {
const { classes } = useStyles() // allow iframe tag
const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : [] const allowList = [...ALLOWED_TAG_LIST, "iframe"]
return ( // memoize component because Interweave is costly
<BasicHtmlStyles> const Content = React.memo((props: ContentProps) => {
<Box className={classes.content}> const { classes } = useStyles()
<Interweave content={props.content} transform={transform} matchers={matchers} /> const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : []
</Box>
</BasicHtmlStyles> return (
) <BasicHtmlStyles>
}) <Box className={classes.content}>
Content.displayName = "Content" <Interweave content={props.content} transform={transform} matchers={matchers} allowList={allowList} />
</Box>
export { Content } </BasicHtmlStyles>
)
})
Content.displayName = "Content"
export { Content }

View File

@@ -1,24 +1,29 @@
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) { export function Enclosure(props: {
const hasVideo = props.enclosureType.startsWith("video") enclosureType: string
const hasAudio = props.enclosureType.startsWith("audio") enclosureUrl: string
const hasImage = props.enclosureType.startsWith("image") }) {
const hasVideo = props.enclosureType.startsWith("video")
return ( const hasAudio = props.enclosureType.startsWith("audio")
<BasicHtmlStyles> const hasImage = props.enclosureType.startsWith("image")
{hasVideo && (
<video controls width="100%"> return (
<source src={props.enclosureUrl} type={props.enclosureType} /> <BasicHtmlStyles>
</video> {hasVideo && (
)} // biome-ignore lint/a11y/useMediaCaption: we don't have any captions for videos
{hasAudio && ( <video controls width="100%">
<audio controls> <source src={props.enclosureUrl} type={props.enclosureType} />
<source src={props.enclosureUrl} type={props.enclosureType} /> </video>
</audio> )}
)} {hasAudio && (
{hasImage && <ImageWithPlaceholderWhileLoading src={props.enclosureUrl} alt="enclosure" />} // biome-ignore lint/a11y/useMediaCaption: we don't have any captions for audio
</BasicHtmlStyles> <audio controls style={{ width: "100%" }}>
) <source src={props.enclosureUrl} type={props.enclosureType} />
} </audio>
)}
{hasImage && <ImageWithPlaceholderWhileLoading src={props.enclosureUrl} alt="enclosure" />}
</BasicHtmlStyles>
)
}

View File

@@ -1,329 +1,313 @@
import { Trans } from "@lingui/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 { Constants } from "app/constants"
import { type ExpendableEntry } from "app/entries/slice" import type { ExpendableEntry } from "app/entries/slice"
import { import {
loadMoreEntries, loadMoreEntries,
markAllEntries, markAllAsReadWithConfirmationIfRequired,
markEntry, markEntry,
reloadEntries, reloadEntries,
selectEntry, selectEntry,
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 { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { selectNextUnreadTreeItem } from "app/tree/thunks"
import { Loader } from "components/Loader" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { Loader } from "components/Loader"
import { useMousetrap } from "hooks/useMousetrap" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useViewMode } from "hooks/useViewMode" import { useMousetrap } from "hooks/useMousetrap"
import { useEffect } from "react" import { useEffect } from "react"
import { useContextMenu } from "react-contexify" import { useContextMenu } from "react-contexify"
import InfiniteScroll from "react-infinite-scroller" import InfiniteScroll from "react-infinite-scroller"
import { throttle } from "throttle-debounce" import { throttle } from "throttle-debounce"
import { FeedEntry } from "./FeedEntry" import { FeedEntry } from "./FeedEntry"
export function FeedEntries() { export function FeedEntries() {
const source = useAppSelector(state => state.entries.source) const entries = useAppSelector(state => state.entries.entries)
const entries = useAppSelector(state => state.entries.entries) const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) const hasMore = useAppSelector(state => state.entries.hasMore)
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId) const loading = useAppSelector(state => state.entries.loading)
const hasMore = useAppSelector(state => state.entries.hasMore) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const loading = useAppSelector(state => state.entries.loading) const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry) const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible) const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu) const dispatch = useAppDispatch()
const { viewMode } = useViewMode() const { openLinkInBackgroundTab } = useBrowserExtension()
const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension() const selectedEntry = entries.find(e => e.id === selectedEntryId)
const selectedEntry = entries.find(e => e.id === selectedEntryId) const headerClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
const middleClick = event.button === 1 || event.ctrlKey || event.metaKey
const headerClicked = (entry: ExpendableEntry, event: React.MouseEvent) => { if (middleClick || viewMode === "expanded") {
const middleClick = event.button === 1 || event.ctrlKey || event.metaKey dispatch(markEntry({ entry, read: true }))
if (middleClick || viewMode === "expanded") { } else if (event.button === 0) {
dispatch(markEntry({ entry, read: true })) // main click
} else if (event.button === 0) { // don't trigger the link
// main click event.preventDefault()
// don't trigger the link
event.preventDefault() dispatch(
selectEntry({
dispatch( entry,
selectEntry({ expand: !entry.expanded,
entry, markAsRead: !entry.expanded,
expand: !entry.expanded, scrollToEntry: true,
markAsRead: !entry.expanded, })
scrollToEntry: true, )
}) }
) }
}
} const contextMenu = useContextMenu()
const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
const contextMenu = useContextMenu() if (event.shiftKey || !customContextMenu) return
const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
if (event.shiftKey || !customContextMenu) return event.preventDefault()
contextMenu.show({
event.preventDefault() id: Constants.dom.entryContextMenuId(entry),
contextMenu.show({ event,
id: Constants.dom.entryContextMenuId(entry), })
event, }
})
} const bodyClicked = (entry: ExpendableEntry) => {
if (viewMode !== "expanded") return
const bodyClicked = (entry: ExpendableEntry) => {
if (viewMode !== "expanded") return // entry is already selected
if (entry.id === selectedEntryId) return
// entry is already selected
if (entry.id === selectedEntryId) return dispatch(
selectEntry({
dispatch( entry,
selectEntry({ expand: true,
entry, markAsRead: true,
expand: true, scrollToEntry: true,
markAsRead: true, })
scrollToEntry: true, )
}) }
)
} const swipedLeft = async (entry: ExpendableEntry) => await dispatch(markEntry({ entry, read: !entry.read }))
const swipedLeft = async (entry: ExpendableEntry) => await dispatch(markEntry({ entry, read: !entry.read })) // close context menu on scroll
useEffect(() => {
// close context menu on scroll const listener = throttle(100, () => contextMenu.hideAll())
useEffect(() => { window.addEventListener("scroll", listener)
const listener = throttle(100, () => contextMenu.hideAll()) return () => window.removeEventListener("scroll", listener)
window.addEventListener("scroll", listener) }, [contextMenu])
return () => window.removeEventListener("scroll", listener)
}, [contextMenu]) useEffect(() => {
const listener = throttle(100, () => {
useEffect(() => { if (viewMode !== "expanded") return
const listener = throttle(100, () => { if (scrollingToEntry) return
if (viewMode !== "expanded") return
if (scrollingToEntry) return const currentEntry = entries
// use slice to get a copy of the array because reverse mutates the array in-place
const currentEntry = entries .slice()
// use slice to get a copy of the array because reverse mutates the array in-place .reverse()
.slice() .find(e => {
.reverse() const el = document.getElementById(Constants.dom.entryId(e))
.find(e => { return el && !Constants.layout.isTopVisible(el)
const el = document.getElementById(Constants.dom.entryId(e)) })
return el && !Constants.layout.isTopVisible(el) if (currentEntry) {
}) dispatch(
if (currentEntry) { selectEntry({
dispatch( entry: currentEntry,
selectEntry({ expand: false,
entry: currentEntry, markAsRead: !!scrollMarks,
expand: false, scrollToEntry: false,
markAsRead: !!scrollMarks, })
scrollToEntry: false, )
}) }
) })
} window.addEventListener("scroll", listener)
}) return () => window.removeEventListener("scroll", listener)
window.addEventListener("scroll", listener) }, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
return () => window.removeEventListener("scroll", listener)
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry]) useMousetrap("r", async () => await dispatch(reloadEntries()))
useMousetrap(
useMousetrap("r", async () => await dispatch(reloadEntries())) "j",
useMousetrap( async () =>
"j", await dispatch(
async () => selectNextEntry({
await dispatch( expand: true,
selectNextEntry({ markAsRead: true,
expand: true, scrollToEntry: true,
markAsRead: true, })
scrollToEntry: true, )
}) )
) useMousetrap(
) "n",
useMousetrap( async () =>
"n", await dispatch(
async () => selectNextEntry({
await dispatch( expand: false,
selectNextEntry({ markAsRead: false,
expand: false, scrollToEntry: true,
markAsRead: false, })
scrollToEntry: true, )
}) )
) useMousetrap(
) "k",
useMousetrap( async () =>
"k", await dispatch(
async () => selectPreviousEntry({
await dispatch( expand: true,
selectPreviousEntry({ markAsRead: true,
expand: true, scrollToEntry: true,
markAsRead: true, })
scrollToEntry: true, )
}) )
) useMousetrap(
) "p",
useMousetrap( async () =>
"p", await dispatch(
async () => selectPreviousEntry({
await dispatch( expand: false,
selectPreviousEntry({ markAsRead: false,
expand: false, scrollToEntry: true,
markAsRead: false, })
scrollToEntry: true, )
}) )
) useMousetrap("shift+j", async () => await dispatch(selectNextUnreadTreeItem({ direction: "forward" })))
) useMousetrap("shift+k", async () => await dispatch(selectNextUnreadTreeItem({ direction: "backward" })))
useMousetrap("space", () => { useMousetrap("space", () => {
if (selectedEntry) { if (selectedEntry) {
if (selectedEntry.expanded) { if (selectedEntry.expanded) {
const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry)) const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry))
if (entryElement && Constants.layout.isBottomVisible(entryElement)) { if (entryElement && Constants.layout.isBottomVisible(entryElement)) {
dispatch( dispatch(
selectNextEntry({ selectNextEntry({
expand: true, expand: true,
markAsRead: true, markAsRead: true,
scrollToEntry: true, scrollToEntry: true,
}) })
) )
} else { } else {
window.scrollTo({ window.scrollTo({
top: window.scrollY + document.documentElement.clientHeight * 0.8, top: window.scrollY + document.documentElement.clientHeight * 0.8,
behavior: "smooth", behavior: "smooth",
}) })
} }
} else { } else {
dispatch( dispatch(
selectEntry({ selectEntry({
entry: selectedEntry, entry: selectedEntry,
expand: true, expand: true,
markAsRead: true, markAsRead: true,
scrollToEntry: true, scrollToEntry: true,
}) })
) )
} }
} else { } else {
dispatch( dispatch(
selectNextEntry({ selectNextEntry({
expand: true, expand: true,
markAsRead: true, markAsRead: true,
scrollToEntry: true, scrollToEntry: true,
}) })
) )
} }
}) })
useMousetrap("shift+space", () => { useMousetrap("shift+space", () => {
if (selectedEntry) { if (selectedEntry) {
if (selectedEntry.expanded) { if (selectedEntry.expanded) {
const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry)) const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry))
if (entryElement && Constants.layout.isTopVisible(entryElement)) { if (entryElement && Constants.layout.isTopVisible(entryElement)) {
dispatch( dispatch(
selectPreviousEntry({ selectPreviousEntry({
expand: true, expand: true,
markAsRead: true, markAsRead: true,
scrollToEntry: true, scrollToEntry: true,
}) })
) )
} else { } else {
window.scrollTo({ window.scrollTo({
top: window.scrollY - document.documentElement.clientHeight * 0.8, top: window.scrollY - document.documentElement.clientHeight * 0.8,
behavior: "smooth", behavior: "smooth",
}) })
} }
} else { } else {
dispatch( dispatch(
selectPreviousEntry({ selectPreviousEntry({
expand: true, expand: true,
markAsRead: true, markAsRead: true,
scrollToEntry: true, scrollToEntry: true,
}) })
) )
} }
} }
}) })
useMousetrap(["o", "enter"], () => { useMousetrap(["o", "enter"], () => {
// toggle expanded status // toggle expanded status
if (!selectedEntry) return if (!selectedEntry) return
dispatch( dispatch(
selectEntry({ selectEntry({
entry: selectedEntry, entry: selectedEntry,
expand: !selectedEntry.expanded, expand: !selectedEntry.expanded,
markAsRead: !selectedEntry.expanded, markAsRead: !selectedEntry.expanded,
scrollToEntry: true, scrollToEntry: true,
}) })
) )
}) })
useMousetrap("v", () => { useMousetrap("v", () => {
// open tab in foreground // open tab in foreground
if (!selectedEntry) return if (!selectedEntry) return
window.open(selectedEntry.url, "_blank", "noreferrer") window.open(selectedEntry.url, "_blank", "noreferrer")
}) })
useMousetrap("b", () => { useMousetrap("b", () => {
if (!selectedEntry) return if (!selectedEntry) return
openLinkInBackgroundTab(selectedEntry.url) openLinkInBackgroundTab(selectedEntry.url)
}) })
useMousetrap("m", () => { useMousetrap("m", () => {
// toggle read status // toggle read status
if (!selectedEntry) return if (!selectedEntry) return
dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read })) dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read }))
}) })
useMousetrap("s", () => { useMousetrap("s", () => {
// toggle starred status // toggle starred status
if (!selectedEntry) return if (!selectedEntry) return
dispatch(starEntry({ entry: selectedEntry, starred: !selectedEntry.starred })) dispatch(starEntry({ entry: selectedEntry, starred: !selectedEntry.starred }))
}) })
useMousetrap("shift+a", () => { useMousetrap("shift+a", () => {
// mark all entries as read // mark all entries as read
dispatch( dispatch(markAllAsReadWithConfirmationIfRequired())
markAllEntries({ })
sourceType: source.type, useMousetrap("g a", async () => await dispatch(redirectToRootCategory()))
req: { useMousetrap("f", () => dispatch(toggleSidebar()))
id: source.id, useMousetrap("?", () =>
read: true, openModal({
olderThan: Date.now(), title: <Trans>Keyboard shortcuts</Trans>,
insertedBefore: entriesTimestamp, size: "xl",
}, children: <KeyboardShortcutsHelp />,
}) })
) )
})
useMousetrap("g a", async () => await dispatch(redirectToRootCategory())) return (
useMousetrap("f", () => dispatch(toggleSidebar())) <InfiniteScroll
useMousetrap("?", () => id="entries"
openModal({ className={`cf-entries cf-view-mode-${viewMode}`}
title: <Trans>Keyboard shortcuts</Trans>, initialLoad={false}
size: "xl", loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
children: <KeyboardShortcutsHelp />, hasMore={hasMore}
}) loader={<Box key={0}>{loading && <Loader />}</Box>}
) >
{entries.map(entry => (
return ( <FeedEntry
<InfiniteScroll key={entry.id}
id="entries" entry={entry}
className={`view-mode-${viewMode}`} expanded={!!entry.expanded || viewMode === "expanded"}
initialLoad={false} selected={entry.id === selectedEntryId}
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))} showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
hasMore={hasMore} maxWidth={sidebarVisible ? Constants.layout.entryMaxWidth : undefined}
loader={<Box key={0}>{loading && <Loader />}</Box>} onHeaderClick={event => headerClicked(entry, event)}
> onHeaderRightClick={event => headerRightClicked(entry, event)}
{entries.map(entry => ( onBodyClick={() => bodyClicked(entry)}
<div onSwipedLeft={async () => await swipedLeft(entry)}
key={entry.id} />
ref={el => { ))}
if (el) el.id = Constants.dom.entryId(entry) </InfiniteScroll>
}} )
> }
<FeedEntry
entry={entry}
expanded={!!entry.expanded || viewMode === "expanded"}
selected={entry.id === selectedEntryId}
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
maxWidth={sidebarVisible ? Constants.layout.entryMaxWidth : undefined}
onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)}
onBodyClick={() => bodyClicked(entry)}
onSwipedLeft={async () => await swipedLeft(entry)}
/>
</div>
))}
</InfiniteScroll>
)
}

View File

@@ -1,191 +1,198 @@
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core" import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { useAppSelector } from "app/store" import { useAppSelector } from "app/store"
import { type Entry, type ViewMode } from "app/types" import type { Entry, ViewMode } from "app/types"
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader" import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader" import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
import { useViewMode } from "hooks/useViewMode" import type React from "react"
import React from "react" import { useSwipeable } from "react-swipeable"
import { useSwipeable } from "react-swipeable" import { tss } from "tss"
import { tss } from "tss" import { FeedEntryBody } from "./FeedEntryBody"
import { FeedEntryBody } from "./FeedEntryBody" import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
import { FeedEntryContextMenu } from "./FeedEntryContextMenu" import { FeedEntryFooter } from "./FeedEntryFooter"
import { FeedEntryFooter } from "./FeedEntryFooter"
interface FeedEntryProps {
interface FeedEntryProps { entry: Entry
entry: Entry expanded: boolean
expanded: boolean selected: boolean
selected: boolean showSelectionIndicator: boolean
showSelectionIndicator: boolean maxWidth?: number
maxWidth?: number onHeaderClick: (e: React.MouseEvent) => void
onHeaderClick: (e: React.MouseEvent) => void onHeaderRightClick: (e: React.MouseEvent) => void
onHeaderRightClick: (e: React.MouseEvent) => void onBodyClick: (e: React.MouseEvent) => void
onBodyClick: (e: React.MouseEvent) => void onSwipedLeft: () => void
onSwipedLeft: () => void }
}
const useStyles = tss
const useStyles = tss .withParams<{
.withParams<{ read: boolean
read: boolean expanded: boolean
expanded: boolean viewMode: ViewMode
viewMode: ViewMode rtl: boolean
rtl: boolean showSelectionIndicator: boolean
showSelectionIndicator: boolean maxWidth?: number
maxWidth?: number fontSizePercentage: number
}>() }>()
.create(({ theme, colorScheme, read, expanded, viewMode, rtl, showSelectionIndicator, maxWidth }) => { .create(({ theme, colorScheme, read, expanded, viewMode, rtl, showSelectionIndicator, maxWidth, fontSizePercentage }) => {
let backgroundColor let backgroundColor: string
if (colorScheme === "dark") { if (colorScheme === "dark") {
backgroundColor = read ? "inherit" : theme.colors.dark[5] backgroundColor = read ? "inherit" : theme.colors.dark[5]
} else { } else {
backgroundColor = read && !expanded ? theme.colors.gray[0] : "inherit" backgroundColor = read && !expanded ? theme.colors.gray[0] : "inherit"
} }
let marginY = 10 let marginY = 10
if (viewMode === "title") { if (viewMode === "title") {
marginY = 2 marginY = 2
} else if (viewMode === "cozy") { } else if (viewMode === "cozy") {
marginY = 6 marginY = 6
} }
let mobileMarginY = 6 let mobileMarginY = 6
if (viewMode === "title") { if (viewMode === "title") {
mobileMarginY = 2 mobileMarginY = 2
} else if (viewMode === "cozy") { } else if (viewMode === "cozy") {
mobileMarginY = 4 mobileMarginY = 4
} }
let backgroundHoverColor = backgroundColor let backgroundHoverColor = backgroundColor
if (!expanded && !read) { if (!expanded && !read) {
backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1] backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
} }
let paperBorderLeftColor let paperBorderLeftColor = ""
if (showSelectionIndicator) { if (showSelectionIndicator) {
const borderLeftColor = colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6] const borderLeftColor = colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6]
paperBorderLeftColor = `${borderLeftColor} !important` paperBorderLeftColor = `${borderLeftColor} !important`
} }
return { return {
paper: { paper: {
backgroundColor, backgroundColor,
borderLeftColor: paperBorderLeftColor, borderLeftColor: paperBorderLeftColor,
marginTop: marginY, marginTop: marginY,
marginBottom: marginY, marginBottom: marginY,
[`@media (max-width: ${Constants.layout.mobileBreakpoint}px)`]: { [`@media (max-width: ${Constants.layout.mobileBreakpoint}px)`]: {
marginTop: mobileMarginY, marginTop: mobileMarginY,
marginBottom: mobileMarginY, marginBottom: mobileMarginY,
}, },
"@media (hover: hover)": { "@media (hover: hover)": {
"&:hover": { "&:hover": {
backgroundColor: backgroundHoverColor, backgroundColor: backgroundHoverColor,
}, },
}, },
}, },
headerLink: { headerLink: {
color: "inherit", fontSize: `${fontSizePercentage}%`,
textDecoration: "none", color: "inherit",
}, textDecoration: "none",
body: { },
direction: rtl ? "rtl" : "ltr", body: {
maxWidth: maxWidth ?? "100%", fontSize: `${fontSizePercentage}%`,
}, direction: rtl ? "rtl" : "ltr",
} maxWidth: maxWidth ?? "100%",
}) },
}
export function FeedEntry(props: FeedEntryProps) { })
const { viewMode } = useViewMode()
const { classes, cx } = useStyles({ export function FeedEntry(props: FeedEntryProps) {
read: props.entry.read, const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
expanded: props.expanded, const fontSizePercentage = useAppSelector(state => state.user.localSettings.fontSizePercentage)
viewMode, const { classes, cx } = useStyles({
rtl: props.entry.rtl, read: props.entry.read,
showSelectionIndicator: props.showSelectionIndicator, expanded: props.expanded,
maxWidth: props.maxWidth, viewMode,
}) rtl: props.entry.rtl,
showSelectionIndicator: props.showSelectionIndicator,
const externalLinkDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode) maxWidth: props.maxWidth,
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode) fontSizePercentage,
const mobile = useMobile() })
const showExternalLinkIcon = const externalLinkDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
externalLinkDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(externalLinkDisplayMode) const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
const showStarIcon = const mobile = useMobile()
props.entry.markable && starIconDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(starIconDisplayMode)
const showExternalLinkIcon =
const swipeHandlers = useSwipeable({ externalLinkDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(externalLinkDisplayMode)
onSwipedLeft: props.onSwipedLeft, const showStarIcon =
}) props.entry.markable && starIconDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(starIconDisplayMode)
let paddingX: MantineSpacing = "xs" const swipeHandlers = useSwipeable({
if (viewMode === "title" || viewMode === "cozy") paddingX = 6 onSwipedLeft: props.onSwipedLeft,
})
let paddingY: MantineSpacing = "xs"
if (viewMode === "title") { let paddingX: MantineSpacing = "xs"
paddingY = 4 if (viewMode === "title" || viewMode === "cozy") paddingX = 6
} else if (viewMode === "cozy") {
paddingY = 8 let paddingY: MantineSpacing = "xs"
} if (viewMode === "title") {
paddingY = 4
let borderRadius: MantineRadius = "sm" } else if (viewMode === "cozy") {
if (viewMode === "title") { paddingY = 8
borderRadius = 0 }
} else if (viewMode === "cozy") {
borderRadius = "xs" let borderRadius: MantineRadius = "sm"
} if (viewMode === "title") {
borderRadius = 0
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy") } else if (viewMode === "cozy") {
return ( borderRadius = "xs"
<Paper }
withBorder
radius={borderRadius} const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
className={cx(classes.paper, { return (
read: props.entry.read, <Paper
unread: !props.entry.read, component="article"
expanded: props.expanded, id={Constants.dom.entryId(props.entry)}
selected: props.selected, data-id={props.entry.id}
"show-selection-indicator": props.showSelectionIndicator, withBorder
})} radius={borderRadius}
> className={cx(classes.paper, {
<a read: props.entry.read,
className={classes.headerLink} unread: !props.entry.read,
href={props.entry.url} expanded: props.expanded,
target="_blank" selected: props.selected,
rel="noreferrer" "show-selection-indicator": props.showSelectionIndicator,
onClick={props.onHeaderClick} })}
onAuxClick={props.onHeaderClick} >
onContextMenu={props.onHeaderRightClick} <a
> className={classes.headerLink}
<Box px={paddingX} py={paddingY} {...swipeHandlers}> href={props.entry.url}
{compactHeader && ( target="_blank"
<FeedEntryCompactHeader rel="noreferrer"
entry={props.entry} onClick={props.onHeaderClick}
showStarIcon={showStarIcon} onAuxClick={props.onHeaderClick}
showExternalLinkIcon={showExternalLinkIcon} onContextMenu={props.onHeaderRightClick}
/> >
)} <Box px={paddingX} py={paddingY} {...swipeHandlers}>
{!compactHeader && ( {compactHeader && (
<FeedEntryHeader <FeedEntryCompactHeader
entry={props.entry} entry={props.entry}
expanded={props.expanded} showStarIcon={showStarIcon}
showStarIcon={showStarIcon} showExternalLinkIcon={showExternalLinkIcon}
showExternalLinkIcon={showExternalLinkIcon} />
/> )}
)} {!compactHeader && (
</Box> <FeedEntryHeader
</a> entry={props.entry}
{props.expanded && ( expanded={props.expanded}
<Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}> showStarIcon={showStarIcon}
<Box className={classes.body}> showExternalLinkIcon={showExternalLinkIcon}
<FeedEntryBody entry={props.entry} /> />
</Box> )}
<Divider variant="dashed" my={paddingY} /> </Box>
<FeedEntryFooter entry={props.entry} /> </a>
</Box> {props.expanded && (
)} <Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
<Box className={`${classes.body} cf-content`}>
<FeedEntryContextMenu entry={props.entry} /> <FeedEntryBody entry={props.entry} />
</Paper> </Box>
) <Divider variant="dashed" my={paddingY} className="cf-footer-divider" />
} <FeedEntryFooter entry={props.entry} />
</Box>
)}
<FeedEntryContextMenu entry={props.entry} />
</Paper>
)
}

View File

@@ -1,37 +1,37 @@
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"
export interface FeedEntryBodyProps { export interface FeedEntryBodyProps {
entry: Entry entry: Entry
} }
export function FeedEntryBody(props: FeedEntryBodyProps) { export function FeedEntryBody(props: FeedEntryBodyProps) {
const search = useAppSelector(state => state.entries.search) const search = useAppSelector(state => state.entries.search)
return ( return (
<Box> <Box>
<Box> <Box>
<Content content={props.entry.content} highlight={search} /> <Content content={props.entry.content} highlight={search} />
</Box> </Box>
{props.entry.enclosureType && props.entry.enclosureUrl && ( {props.entry.enclosureType && props.entry.enclosureUrl && (
<Box pt="md"> <Box pt="md">
<Enclosure enclosureType={props.entry.enclosureType} enclosureUrl={props.entry.enclosureUrl} /> <Enclosure enclosureType={props.entry.enclosureType} enclosureUrl={props.entry.enclosureUrl} />
</Box> </Box>
)} )}
{/* show media only if we don't have content to avoid duplicate content */} {/* show media only if we don't have content to avoid duplicate content */}
{!props.entry.content && props.entry.mediaThumbnailUrl && ( {!props.entry.content && props.entry.mediaThumbnailUrl && (
<Box pt="md"> <Box pt="md">
<Media <Media
thumbnailUrl={props.entry.mediaThumbnailUrl} thumbnailUrl={props.entry.mediaThumbnailUrl}
thumbnailWidth={props.entry.mediaThumbnailWidth} thumbnailWidth={props.entry.mediaThumbnailWidth}
thumbnailHeight={props.entry.mediaThumbnailHeight} thumbnailHeight={props.entry.mediaThumbnailHeight}
description={props.entry.mediaDescription} description={props.entry.mediaDescription}
/> />
</Box> </Box>
)} )}
</Box> </Box>
) )
} }

View File

@@ -1,103 +1,103 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/react/macro"
import { Group } from "@mantine/core" import { Group } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks" import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
import { redirectToFeed } from "app/redirect/thunks" import { redirectToFeed } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { type Entry } from "app/types" import type { Entry } from "app/types"
import { truncate } from "app/utils" import { truncate } from "app/utils"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useColorScheme } from "hooks/useColorScheme" import { useColorScheme } from "hooks/useColorScheme"
import { Item, Menu, Separator } from "react-contexify" import { Item, Menu, Separator } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { tss } from "tss" import { tss } from "tss"
interface FeedEntryContextMenuProps { interface FeedEntryContextMenuProps {
entry: Entry entry: Entry
} }
const iconSize = 16 const iconSize = 16
const useStyles = tss.create(({ theme, colorScheme }) => ({ const useStyles = tss.create(({ theme, colorScheme }) => ({
menu: { menu: {
// apply mantine theme from MenuItem.styles.ts // apply mantine theme from MenuItem.styles.ts
fontSize: theme.fontSizes.sm, fontSize: theme.fontSizes.sm,
"--contexify-item-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`, "--contexify-item-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
"--contexify-activeItem-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`, "--contexify-activeItem-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
"--contexify-activeItem-bgColor": `${colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]} !important`, "--contexify-activeItem-bgColor": `${colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]} !important`,
}, },
})) }))
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) { export function FeedEntryContextMenu(props: 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)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension() const { openLinkInBackgroundTab } = useBrowserExtension()
return ( return (
<Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={colorScheme} animation={false} className={classes.menu}> <Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={colorScheme} animation={false} className={classes.menu}>
<Item <Item
onClick={() => { onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer") window.open(props.entry.url, "_blank", "noreferrer")
dispatch(markEntry({ entry: props.entry, read: true })) dispatch(markEntry({ entry: props.entry, read: true }))
}} }}
> >
<Group> <Group>
<TbExternalLink size={iconSize} /> <TbExternalLink size={iconSize} />
<Trans>Open link in new tab</Trans> <Trans>Open link in new tab</Trans>
</Group> </Group>
</Item> </Item>
<Item <Item
onClick={() => { onClick={() => {
openLinkInBackgroundTab(props.entry.url) openLinkInBackgroundTab(props.entry.url)
dispatch(markEntry({ entry: props.entry, read: true })) dispatch(markEntry({ entry: props.entry, read: true }))
}} }}
> >
<Group> <Group>
<TbExternalLink size={iconSize} /> <TbExternalLink size={iconSize} />
<Trans>Open link in new background tab</Trans> <Trans>Open link in new background tab</Trans>
</Group> </Group>
</Item> </Item>
<Separator /> <Separator />
<Item onClick={async () => await dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}> <Item onClick={async () => await dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}>
<Group> <Group>
{props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />} {props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />}
{props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} {props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
</Group> </Group>
</Item> </Item>
{props.entry.markable && ( {props.entry.markable && (
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}> <Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
<Group> <Group>
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />} {props.entry.read ? <TbMail size={iconSize} /> : <TbMailOpened size={iconSize} />}
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>} {props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
</Group> </Group>
</Item> </Item>
)} )}
<Item onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}> <Item onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}>
<Group> <Group>
<TbArrowBarToDown size={iconSize} /> <TbArrowBarToDown size={iconSize} />
<Trans>Mark as read up to here</Trans> <Trans>Mark as read up to here</Trans>
</Group> </Group>
</Item> </Item>
{sourceType === "category" && ( {sourceType === "category" && (
<> <>
<Separator /> <Separator />
<Item <Item
onClick={() => { onClick={() => {
dispatch(redirectToFeed(props.entry.feedId)) dispatch(redirectToFeed(props.entry.feedId))
}} }}
> >
<Group> <Group>
<TbRss size={iconSize} /> <TbRss size={iconSize} />
<Trans>Go to {truncate(props.entry.feedName, 30)}</Trans> <Trans>Go to {truncate(props.entry.feedName, 30)}</Trans>
</Group> </Group>
</Item> </Item>
</> </>
)} )}
</Menu> </Menu>
) )
} }

View File

@@ -1,102 +1,104 @@
import { t, Trans } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Group, Indicator, Popover, TagsInput } from "@mantine/core" import { useLingui } from "@lingui/react"
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks" import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
import { useAppDispatch, useAppSelector } from "app/store" import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
import { type Entry } from "app/types" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButton" import type { Entry } from "app/types"
import { useActionButton } from "hooks/useActionButton" import { ActionButton } from "components/ActionButton"
import { useMobile } from "hooks/useMobile" import { useActionButton } from "hooks/useActionButton"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb" import { useMobile } from "hooks/useMobile"
import { ShareButtons } from "./ShareButtons" import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { ShareButtons } from "./ShareButtons"
interface FeedEntryFooterProps {
entry: Entry interface FeedEntryFooterProps {
} entry: Entry
}
export function FeedEntryFooter(props: FeedEntryFooterProps) {
const tags = useAppSelector(state => state.user.tags) export function FeedEntryFooter(props: FeedEntryFooterProps) {
const mobile = useMobile() const tags = useAppSelector(state => state.user.tags)
const { spacing } = useActionButton() const mobile = useMobile()
const dispatch = useAppDispatch() const { spacing } = useActionButton()
const dispatch = useAppDispatch()
const readStatusButtonClicked = async () => const { _ } = useLingui()
await dispatch(
markEntry({ const readStatusButtonClicked = async () =>
entry: props.entry, await dispatch(
read: !props.entry.read, markEntry({
}) entry: props.entry,
) read: !props.entry.read,
const onTagsChange = async (values: string[]) => })
await dispatch( )
tagEntry({ const onTagsChange = async (values: string[]) =>
entryId: +props.entry.id, await dispatch(
tags: values, tagEntry({
}) entryId: +props.entry.id,
) tags: values,
})
return ( )
<Group justify="space-between">
<Group gap={spacing}> return (
{props.entry.markable && ( <Group justify="space-between" className="cf-footer">
<ActionButton <Group gap={spacing}>
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />} {props.entry.markable && (
label={props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>} <ActionButton
onClick={readStatusButtonClicked} icon={props.entry.read ? <TbMail size={18} /> : <TbMailOpened size={18} />}
/> label={props.entry.read ? msg`Keep unread` : msg`Mark as read`}
)} onClick={readStatusButtonClicked}
<ActionButton />
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />} )}
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} <ActionButton
onClick={async () => icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
await dispatch( label={props.entry.starred ? msg`Unstar` : msg`Star`}
starEntry({ onClick={async () =>
entry: props.entry, await dispatch(
starred: !props.entry.starred, starEntry({
}) entry: props.entry,
) starred: !props.entry.starred,
} })
/> )
}
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}> />
<Popover.Target>
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} /> <Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
</Popover.Target> <Popover.Target>
<Popover.Dropdown> <ActionButton icon={<TbShare size={18} />} label={msg`Share`} />
<ShareButtons url={props.entry.url} description={props.entry.title} /> </Popover.Target>
</Popover.Dropdown> <Popover.Dropdown>
</Popover> <ShareButtons url={props.entry.url} description={props.entry.title} />
</Popover.Dropdown>
{tags && ( </Popover>
<Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target> {tags && (
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}> <Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} /> <Popover.Target>
</Indicator> <Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
</Popover.Target> <ActionButton icon={<TbTag size={18} />} label={msg`Tags`} />
<Popover.Dropdown> </Indicator>
<TagsInput </Popover.Target>
placeholder={t`Tags`} <Popover.Dropdown>
data={tags} <TagsInput
value={props.entry.tags} placeholder={_(msg`Tags`)}
onChange={onTagsChange} data={tags}
comboboxProps={{ value={props.entry.tags}
withinPortal: false, onChange={onTagsChange}
}} comboboxProps={{
/> withinPortal: false,
</Popover.Dropdown> }}
</Popover> />
)} </Popover.Dropdown>
</Popover>
<a href={props.entry.url} target="_blank" rel="noreferrer"> )}
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
</a> <a href={props.entry.url} target="_blank" rel="noreferrer">
</Group> <ActionButton icon={<TbExternalLink size={18} />} label={msg`Open link`} />
</a>
<ActionButton </Group>
icon={<TbArrowBarToDown size={18} />}
label={<Trans>Mark as read up to here</Trans>} <ActionButton
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))} icon={<TbArrowBarToDown size={18} />}
/> label={msg`Mark as read up to here`}
</Group> onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
) />
} </Group>
)
}

View File

@@ -1,21 +1,21 @@
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 }: FeedFaviconProps) {
return ( return (
<ImageWithPlaceholderWhileLoading <ImageWithPlaceholderWhileLoading
src={url} src={url}
alt="feed favicon" alt="feed favicon"
width={size} width={size}
height={size} height={size}
placeholderWidth={size} placeholderWidth={size}
placeholderHeight={size} placeholderHeight={size}
placeholderBackgroundColor="inherit" placeholderBackgroundColor="inherit"
placeholderIconSize={size} placeholderIconSize={size}
/> />
) )
} }

View File

@@ -1,40 +1,40 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { calculatePlaceholderSize } from "app/utils" import { calculatePlaceholderSize } from "app/utils"
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
import { Content } from "./Content" import { Content } from "./Content"
export interface MediaProps { export interface MediaProps {
thumbnailUrl: string thumbnailUrl: string
thumbnailWidth?: number thumbnailWidth?: number
thumbnailHeight?: number thumbnailHeight?: number
description?: string description?: string
} }
export function Media(props: MediaProps) { export function Media(props: MediaProps) {
const width = props.thumbnailWidth const width = props.thumbnailWidth
const height = props.thumbnailHeight const height = props.thumbnailHeight
const placeholderSize = calculatePlaceholderSize({ const placeholderSize = calculatePlaceholderSize({
width, width,
height, height,
maxWidth: Constants.layout.entryMaxWidth, maxWidth: Constants.layout.entryMaxWidth,
}) })
return ( return (
<BasicHtmlStyles> <BasicHtmlStyles>
<ImageWithPlaceholderWhileLoading <ImageWithPlaceholderWhileLoading
src={props.thumbnailUrl} src={props.thumbnailUrl}
alt="media thumbnail" alt="media thumbnail"
width={props.thumbnailWidth} width={props.thumbnailWidth}
height={props.thumbnailHeight} height={props.thumbnailHeight}
placeholderWidth={placeholderSize.width} placeholderWidth={placeholderSize.width}
placeholderHeight={placeholderSize.height} placeholderHeight={placeholderSize.height}
/> />
{props.description && ( {props.description && (
<Box pt="md"> <Box pt="md">
<Content content={props.description} /> <Content content={props.description} />
</Box> </Box>
)} )}
</BasicHtmlStyles> </BasicHtmlStyles>
) )
} }

View File

@@ -1,113 +1,113 @@
import { Trans } from "@lingui/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 { Constants } from "app/constants"
import { useAppSelector } from "app/store" import { useAppSelector } from "app/store"
import { type SharingSettings } from "app/types" import type { SharingSettings } from "app/types"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
import { type IconType } from "react-icons" import type { IconType } from "react-icons"
import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb" import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb"
import { tss } from "tss" import { tss } from "tss"
type Color = `#${string}` type Color = `#${string}`
const useStyles = tss const useStyles = tss
.withParams<{ .withParams<{
color: Color color: Color
}>() }>()
.create(({ theme, colorScheme, color }) => ({ .create(({ theme, colorScheme, color }) => ({
icon: { icon: {
color, color,
backgroundColor: colorScheme === "dark" ? theme.colors.gray[2] : "white", backgroundColor: colorScheme === "dark" ? theme.colors.gray[2] : "white",
}, },
})) }))
function ShareButton({ icon, color, onClick }: { icon: IconType; color: Color; onClick: () => void }) { function ShareButton({ icon, color, onClick }: { icon: IconType; color: Color; onClick: () => void }) {
const { classes } = useStyles({ const { classes } = useStyles({
color, color,
}) })
return ( return (
<ActionIcon variant="transparent" radius="xl" size={32}> <ActionIcon variant="transparent" radius="xl" size={32}>
<Box p={6} className={classes.icon} onClick={onClick}> <Box p={6} className={classes.icon} onClick={onClick}>
{icon({ size: 18 })} {icon({ size: 18 })}
</Box> </Box>
</ActionIcon> </ActionIcon>
) )
} }
function SiteShareButton({ url, icon, color }: { icon: IconType; color: Color; url: string }) { function SiteShareButton({ url, icon, color }: { 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")
} }
return <ShareButton icon={icon} color={color} onClick={onClick} /> return <ShareButton icon={icon} color={color} onClick={onClick} />
} }
function CopyUrlButton({ url }: { url: string }) { function CopyUrlButton({ url }: { 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} />}
</CopyButton> </CopyButton>
) )
} }
function BrowserNativeShareButton({ url, description }: { url: string; description: string }) { function BrowserNativeShareButton({ url, description }: { url: string; description: string }) {
const mobile = useMobile() const mobile = useMobile()
const { isBrowserExtensionPopup } = useBrowserExtension() const { isBrowserExtensionPopup } = useBrowserExtension()
const onClick = () => { const onClick = () => {
navigator.share({ navigator.share({
title: description, title: description,
url, url,
}) })
} }
return ( return (
<ShareButton <ShareButton
icon={mobile && !isBrowserExtensionPopup ? TbDeviceMobileShare : TbDeviceDesktopShare} icon={mobile && !isBrowserExtensionPopup ? TbDeviceMobileShare : TbDeviceDesktopShare}
color="#000" color="#000"
onClick={onClick} onClick={onClick}
/> />
) )
} }
export function ShareButtons(props: { url: string; description: string }) { export function ShareButtons(props: { 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)
const desc = encodeURIComponent(props.description) const desc = encodeURIComponent(props.description)
const clipboardAvailable = typeof navigator.clipboard !== "undefined" const clipboardAvailable = typeof navigator.clipboard !== "undefined"
const nativeSharingAvailable = typeof navigator.share !== "undefined" const nativeSharingAvailable = typeof navigator.share !== "undefined"
const showNativeSection = clipboardAvailable || nativeSharingAvailable const showNativeSection = clipboardAvailable || nativeSharingAvailable
const showSharingSites = enabledSharingSites.length > 0 const showSharingSites = enabledSharingSites.length > 0
const showDivider = showNativeSection && showSharingSites const showDivider = showNativeSection && showSharingSites
const showNoSharingOptionsAvailable = !showNativeSection && !showSharingSites const showNoSharingOptionsAvailable = !showNativeSection && !showSharingSites
return ( return (
<> <>
{showNativeSection && ( {showNativeSection && (
<SimpleGrid cols={4}> <SimpleGrid cols={4}>
{clipboardAvailable && <CopyUrlButton url={props.url} />} {clipboardAvailable && <CopyUrlButton url={props.url} />}
{nativeSharingAvailable && <BrowserNativeShareButton url={props.url} description={props.description} />} {nativeSharingAvailable && <BrowserNativeShareButton url={props.url} description={props.description} />}
</SimpleGrid> </SimpleGrid>
)} )}
{showDivider && <Divider my="xs" />} {showDivider && <Divider my="xs" />}
{showSharingSites && ( {showSharingSites && (
<SimpleGrid cols={4}> <SimpleGrid cols={4}>
{enabledSharingSites.map(site => ( {enabledSharingSites.map(site => (
<SiteShareButton <SiteShareButton
key={site} key={site}
icon={Constants.sharing[site].icon} icon={Constants.sharing[site].icon}
color={Constants.sharing[site].color} color={Constants.sharing[site].color}
url={Constants.sharing[site].url(url, desc)} url={Constants.sharing[site].url(url, desc)}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>
)} )}
{showNoSharingOptionsAvailable && <Trans>No sharing options available.</Trans>} {showNoSharingOptionsAvailable && <Trans>No sharing options available.</Trans>}
</> </>
) )
} }

View File

@@ -1,50 +1,53 @@
import { t, Trans } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Box, Button, Group, Stack, TextInput } from "@mantine/core" import { useLingui } from "@lingui/react"
import { useForm } from "@mantine/form" import { Trans } from "@lingui/react/macro"
import { client, errorToStrings } from "app/client" import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
import { redirectToSelectedSource } from "app/redirect/thunks" import { useForm } from "@mantine/form"
import { useAppDispatch } from "app/store" import { client, errorToStrings } from "app/client"
import { reloadTree } from "app/tree/thunks" import { redirectToSelectedSource } from "app/redirect/thunks"
import { type AddCategoryRequest } from "app/types" import { useAppDispatch } from "app/store"
import { Alert } from "components/Alert" import { reloadTree } from "app/tree/thunks"
import { useAsyncCallback } from "react-async-hook" import type { AddCategoryRequest } from "app/types"
import { TbFolderPlus } from "react-icons/tb" import { Alert } from "components/Alert"
import { CategorySelect } from "./CategorySelect" import { useAsyncCallback } from "react-async-hook"
import { TbFolderPlus } from "react-icons/tb"
export function AddCategory() { import { CategorySelect } from "./CategorySelect"
const dispatch = useAppDispatch()
export function AddCategory() {
const form = useForm<AddCategoryRequest>() const dispatch = useAppDispatch()
const { _ } = useLingui()
const addCategory = useAsyncCallback(client.category.add, {
onSuccess: () => { const form = useForm<AddCategoryRequest>()
dispatch(reloadTree())
dispatch(redirectToSelectedSource()) const addCategory = useAsyncCallback(client.category.add, {
}, onSuccess: () => {
}) dispatch(reloadTree())
dispatch(redirectToSelectedSource())
return ( },
<> })
{addCategory.error && (
<Box mb="md"> return (
<Alert messages={errorToStrings(addCategory.error)} /> <>
</Box> {addCategory.error && (
)} <Box mb="md">
<Alert messages={errorToStrings(addCategory.error)} />
<form onSubmit={form.onSubmit(addCategory.execute)}> </Box>
<Stack> )}
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable /> <form onSubmit={form.onSubmit(addCategory.execute)}>
<Group justify="center"> <Stack>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}> <TextInput label={<Trans>Category</Trans>} placeholder={_(msg`Category`)} {...form.getInputProps("name")} required />
<Trans>Cancel</Trans> <CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
</Button> <Group justify="center">
<Button type="submit" leftSection={<TbFolderPlus size={16} />} loading={addCategory.loading}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Add</Trans> <Trans>Cancel</Trans>
</Button> </Button>
</Group> <Button type="submit" leftSection={<TbFolderPlus size={16} />} loading={addCategory.loading}>
</Stack> <Trans>Add</Trans>
</form> </Button>
</> </Group>
) </Stack>
} </form>
</>
)
}

View File

@@ -1,51 +1,55 @@
import { t } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Select, type SelectProps } from "@mantine/core" import { useLingui } from "@lingui/react"
import { type ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types" import { Select, type SelectProps } from "@mantine/core"
import { Constants } from "app/constants" import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { useAppSelector } from "app/store" import { Constants } from "app/constants"
import { type Category } from "app/types" import { useAppSelector } from "app/store"
import { flattenCategoryTree } from "app/utils" import type { Category } from "app/types"
import { flattenCategoryTree } from "app/utils"
type CategorySelectProps = Partial<SelectProps> & {
withAll?: boolean type CategorySelectProps = Partial<SelectProps> & {
withoutCategoryIds?: string[] withAll?: boolean
} withoutCategoryIds?: string[]
}
export function CategorySelect(props: CategorySelectProps) {
const rootCategory = useAppSelector(state => state.tree.rootCategory) export function CategorySelect(props: CategorySelectProps) {
const categories = rootCategory && flattenCategoryTree(rootCategory) const rootCategory = useAppSelector(state => state.tree.rootCategory)
const categoriesById = categories?.reduce((map, c) => { const { _ } = useLingui()
map.set(c.id, c)
return map const categories = rootCategory && flattenCategoryTree(rootCategory)
}, new Map<string, Category>()) const categoriesById = categories?.reduce((map, c) => {
const categoryLabel = (cat: Category) => { map.set(c.id, c)
let label = cat.name return map
}, new Map<string, Category>())
while (cat.parentId) { const categoryLabel = (category: Category) => {
const parent = categoriesById?.get(cat.parentId) let cat = category
if (!parent) { let label = cat.name
break
} while (cat.parentId) {
label = `${parent.name}${label}` const parent = categoriesById?.get(cat.parentId)
cat = parent if (!parent) {
} break
}
return label label = `${parent.name}${label}`
} cat = parent
const selectData: ComboboxItem[] | undefined = categories }
?.filter(c => c.id !== Constants.categories.all.id)
.filter(c => !props.withoutCategoryIds?.includes(c.id)) return label
.map(c => ({ }
label: categoryLabel(c), const selectData: ComboboxItem[] | undefined = categories
value: c.id, ?.filter(c => c.id !== Constants.categories.all.id)
})) .filter(c => !props.withoutCategoryIds?.includes(c.id))
.sort((c1, c2) => c1.label.localeCompare(c2.label)) .map(c => ({
if (props.withAll) { label: categoryLabel(c),
selectData?.unshift({ value: c.id,
label: t`All`, }))
value: Constants.categories.all.id, .sort((c1, c2) => c1.label.localeCompare(c2.label))
}) if (props.withAll) {
} selectData?.unshift({
label: _(msg`All`),
return <Select {...props} data={selectData ?? []} disabled={!selectData} /> value: Constants.categories.all.id,
} })
}
return <Select {...props} data={selectData ?? []} disabled={!selectData} />
}

View File

@@ -1,64 +1,67 @@
import { t, Trans } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Box, Button, FileInput, Group, Stack } from "@mantine/core" import { useLingui } from "@lingui/react"
import { isNotEmpty, useForm } from "@mantine/form" import { Trans } from "@lingui/react/macro"
import { client, errorToStrings } from "app/client" import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
import { redirectToSelectedSource } from "app/redirect/thunks" import { isNotEmpty, useForm } from "@mantine/form"
import { useAppDispatch } from "app/store" import { client, errorToStrings } from "app/client"
import { reloadTree } from "app/tree/thunks" import { redirectToSelectedSource } from "app/redirect/thunks"
import { Alert } from "components/Alert" import { useAppDispatch } from "app/store"
import { useAsyncCallback } from "react-async-hook" import { reloadTree } from "app/tree/thunks"
import { TbFileImport } from "react-icons/tb" import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook"
export function ImportOpml() { import { TbFileImport } from "react-icons/tb"
const dispatch = useAppDispatch()
export function ImportOpml() {
const form = useForm<{ file: File }>({ const dispatch = useAppDispatch()
validate: { const { _ } = useLingui()
file: isNotEmpty(t`OPML file is required`),
}, const form = useForm<{ file: File }>({
}) validate: {
file: isNotEmpty(_(msg`OPML file is required`)),
const importOpml = useAsyncCallback(client.feed.importOpml, { },
onSuccess: () => { })
dispatch(reloadTree())
dispatch(redirectToSelectedSource()) const importOpml = useAsyncCallback(client.feed.importOpml, {
}, onSuccess: () => {
}) dispatch(reloadTree())
dispatch(redirectToSelectedSource())
return ( },
<> })
{importOpml.error && (
<Box mb="md"> return (
<Alert messages={errorToStrings(importOpml.error)} /> <>
</Box> {importOpml.error && (
)} <Box mb="md">
<Alert messages={errorToStrings(importOpml.error)} />
<form onSubmit={form.onSubmit(async v => await importOpml.execute(v.file))}> </Box>
<Stack> )}
<FileInput
label={<Trans>OPML file</Trans>} <form onSubmit={form.onSubmit(async v => await importOpml.execute(v.file))}>
leftSection={<TbFileImport />} <Stack>
placeholder={t`OPML file`} <FileInput
description={ label={<Trans>OPML file</Trans>}
<Trans> leftSection={<TbFileImport />}
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your placeholder={_(msg`OPML file`)}
data from other feed reading services. description={
</Trans> <Trans>
} An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your
{...form.getInputProps("file")} data from other feed reading services.
required </Trans>
accept=".xml,.opml" }
/> {...form.getInputProps("file")}
<Group justify="center"> required
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}> accept=".xml,.opml"
<Trans>Cancel</Trans> />
</Button> <Group justify="center">
<Button type="submit" leftSection={<TbFileImport size={16} />} loading={importOpml.loading}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Import</Trans> <Trans>Cancel</Trans>
</Button> </Button>
</Group> <Button type="submit" leftSection={<TbFileImport size={16} />} loading={importOpml.loading}>
</Stack> <Trans>Import</Trans>
</form> </Button>
</> </Group>
) </Stack>
} </form>
</>
)
}

View File

@@ -1,129 +1,129 @@
import { Trans } from "@lingui/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 { client, errorToStrings } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks" import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { reloadTree } from "app/tree/thunks" import { reloadTree } from "app/tree/thunks"
import { type FeedInfoRequest, type SubscribeRequest } from "app/types" import type { FeedInfoRequest, SubscribeRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useState } from "react" import { useState } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbRss } from "react-icons/tb" import { TbRss } from "react-icons/tb"
import { CategorySelect } from "./CategorySelect" import { CategorySelect } from "./CategorySelect"
export function Subscribe() { export function Subscribe() {
const [activeStep, setActiveStep] = useState(0) const [activeStep, setActiveStep] = useState(0)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const step0Form = useForm<FeedInfoRequest>({ const step0Form = useForm<FeedInfoRequest>({
initialValues: { initialValues: {
url: "", url: "",
}, },
}) })
const step1Form = useForm<SubscribeRequest>({ const step1Form = useForm<SubscribeRequest>({
initialValues: { initialValues: {
url: "", url: "",
title: "", title: "",
categoryId: Constants.categories.all.id, categoryId: Constants.categories.all.id,
}, },
}) })
const fetchFeed = useAsyncCallback(client.feed.fetchFeed, { const fetchFeed = useAsyncCallback(client.feed.fetchFeed, {
onSuccess: ({ data }) => { onSuccess: ({ data }) => {
step1Form.setFieldValue("url", data.url) step1Form.setFieldValue("url", data.url)
step1Form.setFieldValue("title", data.title) step1Form.setFieldValue("title", data.title)
setActiveStep(step => step + 1) setActiveStep(step => step + 1)
}, },
}) })
const subscribe = useAsyncCallback(client.feed.subscribe, { const subscribe = useAsyncCallback(client.feed.subscribe, {
onSuccess: sub => { onSuccess: async sub => {
dispatch(reloadTree()) await dispatch(reloadTree())
dispatch(redirectToFeed(sub.data)) dispatch(redirectToFeed(sub.data))
}, },
}) })
const previousStep = () => { const previousStep = () => {
if (activeStep === 0) { if (activeStep === 0) {
dispatch(redirectToSelectedSource()) dispatch(redirectToSelectedSource())
} else { } else {
setActiveStep(activeStep - 1) setActiveStep(activeStep - 1)
} }
} }
const nextStep = (e: React.FormEvent<HTMLFormElement>) => { const nextStep = (e: React.FormEvent<HTMLFormElement>) => {
if (activeStep === 0) { if (activeStep === 0) {
step0Form.onSubmit(fetchFeed.execute)(e) step0Form.onSubmit(fetchFeed.execute)(e)
} else if (activeStep === 1) { } else if (activeStep === 1) {
step1Form.onSubmit(subscribe.execute)(e) step1Form.onSubmit(subscribe.execute)(e)
} }
} }
return ( return (
<> <>
{fetchFeed.error && ( {fetchFeed.error && (
<Box mb="md"> <Box mb="md">
<Alert messages={errorToStrings(fetchFeed.error)} /> <Alert messages={errorToStrings(fetchFeed.error)} />
</Box> </Box>
)} )}
{subscribe.error && ( {subscribe.error && (
<Box mb="md"> <Box mb="md">
<Alert messages={errorToStrings(subscribe.error)} /> <Alert messages={errorToStrings(subscribe.error)} />
</Box> </Box>
)} )}
<form onSubmit={nextStep}> <form onSubmit={nextStep}>
<Stepper active={activeStep} onStepClick={setActiveStep}> <Stepper active={activeStep} onStepClick={setActiveStep}>
<Stepper.Step <Stepper.Step
label={<Trans>Analyze feed</Trans>} label={<Trans>Analyze feed</Trans>}
description={<Trans>Check that the feed is working</Trans>} description={<Trans>Check that the feed is working</Trans>}
allowStepSelect={activeStep === 1} allowStepSelect={activeStep === 1}
> >
<TextInput <TextInput
label={<Trans>Feed URL</Trans>} label={<Trans>Feed URL</Trans>}
placeholder="https://www.mysite.com/rss" placeholder="https://www.mysite.com/rss"
description={ description={
<Trans> <Trans>
The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed 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. will try to find the feed in the page.
</Trans> </Trans>
} }
required required
autoFocus autoFocus
{...step0Form.getInputProps("url")} {...step0Form.getInputProps("url")}
/> />
</Stepper.Step> </Stepper.Step>
<Stepper.Step <Stepper.Step
label={<Trans>Subscribe</Trans>} label={<Trans>Subscribe</Trans>}
description={<Trans>Subscribe to the feed</Trans>} description={<Trans>Subscribe to the feed</Trans>}
allowStepSelect={false} allowStepSelect={false}
> >
<Stack> <Stack>
<TextInput label={<Trans>Feed URL</Trans>} {...step1Form.getInputProps("url")} disabled /> <TextInput label={<Trans>Feed URL</Trans>} {...step1Form.getInputProps("url")} disabled />
<TextInput label={<Trans>Feed name</Trans>} {...step1Form.getInputProps("title")} required autoFocus /> <TextInput label={<Trans>Feed name</Trans>} {...step1Form.getInputProps("title")} required autoFocus />
<CategorySelect label={<Trans>Category</Trans>} {...step1Form.getInputProps("categoryId")} clearable /> <CategorySelect label={<Trans>Category</Trans>} {...step1Form.getInputProps("categoryId")} clearable />
</Stack> </Stack>
</Stepper.Step> </Stepper.Step>
</Stepper> </Stepper>
<Group justify="center" mt="xl"> <Group justify="center" mt="xl">
<Button variant="default" onClick={previousStep}> <Button variant="default" onClick={previousStep}>
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
{activeStep === 0 && ( {activeStep === 0 && (
<Button type="submit" loading={fetchFeed.loading}> <Button type="submit" loading={fetchFeed.loading}>
<Trans>Next</Trans> <Trans>Next</Trans>
</Button> </Button>
)} )}
{activeStep === 1 && ( {activeStep === 1 && (
<Button type="submit" leftSection={<TbRss size={16} />} loading={fetchFeed.loading || subscribe.loading}> <Button type="submit" leftSection={<TbRss size={16} />} loading={fetchFeed.loading || subscribe.loading}>
<Trans>Subscribe</Trans> <Trans>Subscribe</Trans>
</Button> </Button>
)} )}
</Group> </Group>
</form> </form>
</> </>
) )
} }

View File

@@ -1,72 +1,72 @@
import { Box, Text } from "@mantine/core" import { Box } from "@mantine/core"
import { type Entry } from "app/types" import type { Entry } from "app/types"
import { FeedFavicon } from "components/content/FeedFavicon" import { RelativeDate } from "components/RelativeDate"
import { OpenExternalLink } from "components/content/header/OpenExternalLink" import { FeedFavicon } from "components/content/FeedFavicon"
import { Star } from "components/content/header/Star" import { OpenExternalLink } from "components/content/header/OpenExternalLink"
import { RelativeDate } from "components/RelativeDate" import { Star } from "components/content/header/Star"
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 {
entry: Entry entry: Entry
showStarIcon?: boolean showStarIcon?: boolean
showExternalLinkIcon?: boolean showExternalLinkIcon?: boolean
} }
const useStyles = tss const useStyles = tss
.withParams<{ .withParams<{
read: boolean read: boolean
}>() }>()
.create(({ colorScheme, read }) => ({ .create(({ colorScheme, read }) => ({
wrapper: { wrapper: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
columnGap: "10px", columnGap: "10px",
}, },
title: { title: {
flexGrow: 1, flexGrow: 1,
fontWeight: colorScheme === "light" && !read ? "bold" : "inherit", fontWeight: colorScheme === "light" && !read ? "bold" : "inherit",
whiteSpace: "nowrap", whiteSpace: "nowrap",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
}, },
feedName: { feedName: {
width: "145px", width: "145px",
minWidth: "145px", minWidth: "145px",
whiteSpace: "nowrap", whiteSpace: "nowrap",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
}, },
date: { date: {
whiteSpace: "nowrap", whiteSpace: "nowrap",
}, },
})) }))
export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) { export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
const { classes } = useStyles({ const { classes } = useStyles({
read: props.entry.read, read: props.entry.read,
}) })
return ( return (
<Box className={classes.wrapper}> <Box className={classes.wrapper}>
{props.showStarIcon && <Star entry={props.entry} />} {props.showStarIcon && <Star entry={props.entry} />}
<Box> <Box>
<FeedFavicon url={props.entry.iconUrl} /> <FeedFavicon url={props.entry.iconUrl} />
</Box> </Box>
<OnDesktop> <OnDesktop>
<Text c="dimmed" className={classes.feedName}> <Box c="dimmed" className={classes.feedName}>
{props.entry.feedName} {props.entry.feedName}
</Text> </Box>
</OnDesktop> </OnDesktop>
<Box className={classes.title}> <Box className={classes.title}>
<FeedEntryTitle entry={props.entry} /> <FeedEntryTitle entry={props.entry} />
</Box> </Box>
<OnDesktop> <OnDesktop>
<Text c="dimmed" className={classes.date}> <Box c="dimmed" className={classes.date}>
<RelativeDate date={props.entry.date} /> <RelativeDate date={props.entry.date} />
</Text> </Box>
</OnDesktop> </OnDesktop>
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />} {props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
</Box> </Box>
) )
} }

View File

@@ -1,67 +1,62 @@
import { Box, Flex, Space, Text } from "@mantine/core" import { Box, Flex, Space } from "@mantine/core"
import { type Entry } from "app/types" import type { Entry } from "app/types"
import { FeedFavicon } from "components/content/FeedFavicon" import { RelativeDate } from "components/RelativeDate"
import { OpenExternalLink } from "components/content/header/OpenExternalLink" import { FeedFavicon } from "components/content/FeedFavicon"
import { Star } from "components/content/header/Star" import { OpenExternalLink } from "components/content/header/OpenExternalLink"
import { RelativeDate } from "components/RelativeDate" import { Star } from "components/content/header/Star"
import { tss } from "tss" import { tss } from "tss"
import { FeedEntryTitle } from "./FeedEntryTitle" import { FeedEntryTitle } from "./FeedEntryTitle"
export interface FeedEntryHeaderProps { export interface FeedEntryHeaderProps {
entry: Entry entry: Entry
expanded: boolean expanded: boolean
showStarIcon?: boolean showStarIcon?: boolean
showExternalLinkIcon?: boolean showExternalLinkIcon?: boolean
} }
const useStyles = tss const useStyles = tss
.withParams<{ .withParams<{
read: boolean read: boolean
}>() }>()
.create(({ colorScheme, read }) => ({ .create(({ colorScheme, read }) => ({
main: { main: {
fontWeight: colorScheme === "light" && !read ? "bold" : "inherit", fontWeight: colorScheme === "light" && !read ? "bold" : "inherit",
}, },
details: { }))
fontSize: "90%",
}, export function FeedEntryHeader(props: FeedEntryHeaderProps) {
})) const { classes } = useStyles({
read: props.entry.read,
export function FeedEntryHeader(props: FeedEntryHeaderProps) { })
const { classes } = useStyles({ return (
read: props.entry.read, <Box className="cf-header">
}) <Flex align="flex-start" justify="space-between" className="cf-header-title">
return ( <Flex align="flex-start" className={classes.main}>
<Box> {props.showStarIcon && (
<Flex align="flex-start" justify="space-between"> <Box ml={-5}>
<Flex align="flex-start" className={classes.main}> <Star entry={props.entry} />
{props.showStarIcon && ( </Box>
<Box ml={-5}> )}
<Star entry={props.entry} /> <FeedEntryTitle entry={props.entry} />
</Box> </Flex>
)} {props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
<FeedEntryTitle entry={props.entry} /> </Flex>
</Flex> <Flex align="center" className="cf-header-subtitle">
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />} <FeedFavicon url={props.entry.iconUrl} />
</Flex> <Space w={6} />
<Flex align="center" className={classes.details}> <Box c="dimmed">
<FeedFavicon url={props.entry.iconUrl} /> {props.entry.feedName}
<Space w={6} /> <span> · </span>
<Text c="dimmed"> <RelativeDate date={props.entry.date} />
{props.entry.feedName} </Box>
<span> · </span> </Flex>
<RelativeDate date={props.entry.date} /> {props.expanded && (
</Text> <Box className="cf-header-details">
</Flex> {props.entry.author && <span>by {props.entry.author}</span>}
{props.expanded && ( {props.entry.author && props.entry.categories && <span>&nbsp;·&nbsp;</span>}
<Box className={classes.details}> {props.entry.categories && <span>{props.entry.categories}</span>}
<Text c="dimmed"> </Box>
{props.entry.author && <span>by {props.entry.author}</span>} )}
{props.entry.author && props.entry.categories && <span>&nbsp;·&nbsp;</span>} </Box>
{props.entry.categories && <span>{props.entry.categories}</span>} )
</Text> }
</Box>
)}
</Box>
)
}

View File

@@ -1,22 +1,22 @@
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: 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 (
<Highlight <Highlight
inherit inherit
highlight={keywords ?? ""} highlight={keywords ?? ""}
// make sure ellipsis is shown when title is too long // make sure ellipsis is shown when title is too long
span span
> >
{props.entry.title} {props.entry.title}
</Highlight> </Highlight>
) )
} }

View File

@@ -1,30 +1,30 @@
import { Trans } from "@lingui/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 { Constants } from "app/constants"
import { markEntry } from "app/entries/thunks" import { markEntry } from "app/entries/thunks"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { type Entry } from "app/types" import type { Entry } from "app/types"
import { TbExternalLink } from "react-icons/tb" import { TbExternalLink } from "react-icons/tb"
export function OpenExternalLink(props: { entry: Entry }) { export function OpenExternalLink(props: { entry: Entry }) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const onClick = (e: React.MouseEvent) => { const onClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
dispatch( dispatch(
markEntry({ markEntry({
entry: props.entry, entry: props.entry,
read: true, read: true,
}) })
) )
} }
return ( return (
<Anchor href={props.entry.url} target="_blank" rel="noreferrer" onClick={onClick}> <Anchor href={props.entry.url} target="_blank" rel="noreferrer" onClick={onClick}>
<Tooltip label={<Trans>Open link</Trans>} openDelay={Constants.tooltip.delay}> <Tooltip label={<Trans>Open link</Trans>} openDelay={Constants.tooltip.delay}>
<ActionIcon variant="transparent" c="dimmed"> <ActionIcon variant="transparent" c="dimmed">
<TbExternalLink size={18} /> <TbExternalLink size={18} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Anchor> </Anchor>
) )
} }

View File

@@ -1,29 +1,29 @@
import { Trans } from "@lingui/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 { Constants } from "app/constants"
import { starEntry } from "app/entries/thunks" import { starEntry } from "app/entries/thunks"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import type { Entry } from "app/types" import type { Entry } from "app/types"
import { TbStar, TbStarFilled } from "react-icons/tb" import { TbStar, TbStarFilled } from "react-icons/tb"
export function Star(props: { entry: Entry }) { export function Star(props: { entry: Entry }) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const onClick = (e: React.MouseEvent) => { const onClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
dispatch( dispatch(
starEntry({ starEntry({
entry: props.entry, entry: props.entry,
starred: !props.entry.starred, starred: !props.entry.starred,
}) })
) )
} }
return ( return (
<Tooltip label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} openDelay={Constants.tooltip.delay}> <Tooltip label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} openDelay={Constants.tooltip.delay}>
<ActionIcon variant="transparent" onClick={onClick}> <ActionIcon variant="transparent" onClick={onClick}>
{props.entry.starred ? <TbStarFilled size={18} /> : <TbStar size={18} />} {props.entry.starred ? <TbStarFilled size={18} /> : <TbStar size={18} />}
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
) )
} }

View File

@@ -1,169 +1,178 @@
import { t, Trans } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core" import { useLingui } from "@lingui/react"
import { useForm } from "@mantine/form" import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks" import { useForm } from "@mantine/form"
import { useAppDispatch, useAppSelector } from "app/store" import { markAllAsReadWithConfirmationIfRequired, reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
import { changeReadingMode, changeReadingOrder } from "app/user/thunks" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButton" import { changeReadingMode, changeReadingOrder } from "app/user/thunks"
import { Loader } from "components/Loader" import { ActionButton } from "components/ActionButton"
import { useActionButton } from "hooks/useActionButton" import { Loader } from "components/Loader"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useActionButton } from "hooks/useActionButton"
import { useMobile } from "hooks/useMobile" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useEffect } from "react" import { useMobile } from "hooks/useMobile"
import { import { useEffect } from "react"
TbArrowDown, import {
TbArrowUp, TbArrowDown,
TbExternalLink, TbArrowUp,
TbEye, TbChecks,
TbEyeOff, TbExternalLink,
TbRefresh, TbEye,
TbSearch, TbEyeOff,
TbSettings, TbRefresh,
TbSortAscending, TbSearch,
TbSortDescending, TbSettings,
TbUser, TbSortAscending,
} from "react-icons/tb" TbSortDescending,
import { MarkAllAsReadButton } from "./MarkAllAsReadButton" TbUser,
import { ProfileMenu } from "./ProfileMenu" } from "react-icons/tb"
import { ProfileMenu } from "./ProfileMenu"
function HeaderDivider() {
return <Divider orientation="vertical" /> function HeaderDivider() {
} return <Divider orientation="vertical" />
}
function HeaderToolbar(props: { children: React.ReactNode }) {
const { spacing } = useActionButton() function HeaderToolbar(props: { children: React.ReactNode }) {
const mobile = useMobile("480px") const { spacing } = useActionButton()
return mobile ? ( const mobile = useMobile("480px")
// on mobile use all available width return mobile ? (
<Box // on mobile use all available width
style={{ <Box
width: "100%", style={{
display: "flex", width: "100%",
justifyContent: "space-between", display: "flex",
}} justifyContent: "space-between",
> }}
{props.children} className="cf-toolbar"
</Box> >
) : ( {props.children}
<Group gap={spacing}>{props.children}</Group> </Box>
) ) : (
} <Group gap={spacing} className="cf-toolbar">
{props.children}
const iconSize = 18 </Group>
)
export function Header() { }
const settings = useAppSelector(state => state.user.settings)
const profile = useAppSelector(state => state.user.profile) const iconSize = 18
const searchFromStore = useAppSelector(state => state.entries.search)
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension() export function Header() {
const dispatch = useAppDispatch() const settings = useAppSelector(state => state.user.settings)
const profile = useAppSelector(state => state.user.profile)
const searchForm = useForm<{ search: string }>({ const searchFromStore = useAppSelector(state => state.entries.search)
validate: { const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
search: value => (value.length > 0 && value.length < 3 ? t`Search requires at least 3 characters` : null), const dispatch = useAppDispatch()
}, const { _ } = useLingui()
})
const { setValues } = searchForm const searchForm = useForm<{ search: string }>({
validate: {
useEffect(() => { search: value => (value.length > 0 && value.length < 3 ? _(msg`Search requires at least 3 characters`) : null),
setValues({ },
search: searchFromStore, })
}) const { setValues } = searchForm
}, [setValues, searchFromStore])
useEffect(() => {
if (!settings) return <Loader /> setValues({
return ( search: searchFromStore,
<Center> })
<HeaderToolbar> }, [setValues, searchFromStore])
<ActionButton
icon={<TbArrowUp size={iconSize} />} if (!settings) return <Loader />
label={<Trans>Previous</Trans>} return (
onClick={async () => <Center className="cf-toolbar-wrapper">
await dispatch( <HeaderToolbar>
selectPreviousEntry({ <ActionButton
expand: true, icon={<TbArrowUp size={iconSize} />}
markAsRead: true, label={msg`Previous`}
scrollToEntry: true, onClick={async () =>
}) await dispatch(
) selectPreviousEntry({
} expand: true,
/> markAsRead: true,
<ActionButton scrollToEntry: true,
icon={<TbArrowDown size={iconSize} />} })
label={<Trans>Next</Trans>} )
onClick={async () => }
await dispatch( />
selectNextEntry({ <ActionButton
expand: true, icon={<TbArrowDown size={iconSize} />}
markAsRead: true, label={msg`Next`}
scrollToEntry: true, onClick={async () =>
}) await dispatch(
) selectNextEntry({
} expand: true,
/> markAsRead: true,
scrollToEntry: true,
<HeaderDivider /> })
)
<ActionButton }
icon={<TbRefresh size={iconSize} />} />
label={<Trans>Refresh</Trans>}
onClick={async () => await dispatch(reloadEntries())} <HeaderDivider />
/>
<MarkAllAsReadButton iconSize={iconSize} /> <ActionButton
icon={<TbRefresh size={iconSize} />}
<HeaderDivider /> label={msg`Refresh`}
onClick={async () => await dispatch(reloadEntries())}
<ActionButton />
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />} <ActionButton
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>} icon={<TbChecks size={iconSize} />}
onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))} label={msg`Mark all as read`}
/> onClick={() => dispatch(markAllAsReadWithConfirmationIfRequired())}
<ActionButton />
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>} <HeaderDivider />
onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
/> <ActionButton
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
<Popover> label={settings.readingMode === "all" ? msg`All` : msg`Unread`}
<Popover.Target> onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
<Indicator disabled={!searchFromStore}> />
<ActionButton icon={<TbSearch size={iconSize} />} label={<Trans>Search</Trans>} /> <ActionButton
</Indicator> icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
</Popover.Target> label={settings.readingOrder === "asc" ? msg`Asc` : msg`Desc`}
<Popover.Dropdown> onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}> />
<TextInput
placeholder={t`Search`} <Popover>
{...searchForm.getInputProps("search")} <Popover.Target>
leftSection={<TbSearch size={iconSize} />} <Indicator disabled={!searchFromStore}>
rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />} <ActionButton icon={<TbSearch size={iconSize} />} label={msg`Search`} />
autoFocus </Indicator>
/> </Popover.Target>
</form> <Popover.Dropdown>
</Popover.Dropdown> <form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
</Popover> <TextInput
placeholder={_(msg`Search`)}
<HeaderDivider /> {...searchForm.getInputProps("search")}
leftSection={<TbSearch size={iconSize} />}
<ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} /> rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
autoFocus
{isBrowserExtensionPopup && ( />
<> </form>
<HeaderDivider /> </Popover.Dropdown>
</Popover>
<ActionButton
icon={<TbSettings size={iconSize} />} <HeaderDivider />
label={<Trans>Extension options</Trans>}
onClick={() => openSettingsPage()} <ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} />
/>
<ActionButton {isBrowserExtensionPopup && (
icon={<TbExternalLink size={iconSize} />} <>
label={<Trans>Open CommaFeed</Trans>} <HeaderDivider />
onClick={() => openAppInNewTab()}
/> <ActionButton
</> icon={<TbSettings size={iconSize} />}
)} label={msg`Extension options`}
</HeaderToolbar> onClick={() => openSettingsPage()}
</Center> />
) <ActionButton
} icon={<TbExternalLink size={iconSize} />}
label={msg`Open CommaFeed`}
onClick={() => openAppInNewTab()}
/>
</>
)}
</HeaderToolbar>
</Center>
)
}

View File

@@ -1,97 +0,0 @@
import { Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButton"
import { useState } from "react"
import { TbChecks } from "react-icons/tb"
export function MarkAllAsReadButton(props: { iconSize: number }) {
const [opened, setOpened] = useState(false)
const [threshold, setThreshold] = useState(0)
const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
const dispatch = useAppDispatch()
const buttonClicked = () => {
if (markAllAsReadConfirmation) {
setThreshold(0)
setOpened(true)
} else {
dispatch(
markAllEntries({
sourceType: source.type,
req: {
id: source.id,
read: true,
olderThan: Date.now(),
insertedBefore: entriesTimestamp,
},
})
)
}
}
return (
<>
<Modal opened={opened} onClose={() => setOpened(false)} title={<Trans>Mark all entries as read</Trans>}>
<Stack>
<Text size="sm">
{threshold === 0 && (
<Trans>
Are you sure you want to mark all entries of <Code>{sourceLabel}</Code> as read?
</Trans>
)}
{threshold > 0 && (
<Trans>
Are you sure you want to mark entries older than {threshold} days of <Code>{sourceLabel}</Code> as read?
</Trans>
)}
</Text>
<Slider
py="xl"
min={0}
max={28}
marks={[
{ value: 0, label: "0" },
{ value: 7, label: "7" },
{ value: 14, label: "14" },
{ value: 21, label: "21" },
{ value: 28, label: "28" },
]}
value={threshold}
onChange={setThreshold}
/>
<Group justify="flex-end">
<Button variant="default" onClick={() => setOpened(false)}>
<Trans>Cancel</Trans>
</Button>
<Button
color="red"
onClick={() => {
setOpened(false)
dispatch(
markAllEntries({
sourceType: source.type,
req: {
id: source.id,
read: true,
olderThan: Date.now() - threshold * 24 * 60 * 60 * 1000,
insertedBefore: entriesTimestamp,
},
})
)
}}
>
<Trans>Confirm</Trans>
</Button>
</Group>
</Stack>
</Modal>
<ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
</>
)
}

View File

@@ -1,217 +1,259 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/react/macro"
import { import {
Box, Box,
Divider, Divider,
Group, Group,
type MantineColorScheme, type MantineColorScheme,
Menu, Menu,
SegmentedControl, SegmentedControl,
type SegmentedControlItem, type SegmentedControlItem,
useMantineColorScheme, Slider,
} from "@mantine/core" useMantineColorScheme,
import { showNotification } from "@mantine/notifications" } from "@mantine/core"
import { client } from "app/client" import { showNotification } from "@mantine/notifications"
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks" import { client } from "app/client"
import { useAppDispatch, useAppSelector } from "app/store" import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
import { type ViewMode } from "app/types" import { useAppDispatch, useAppSelector } from "app/store"
import { useViewMode } from "hooks/useViewMode" import type { ViewMode } from "app/types"
import { type ReactNode, useState } from "react" import { setFontSizePercentage, setViewMode } from "app/user/slice"
import { import { reloadProfile } from "app/user/thunks"
TbChartLine, import dayjs from "dayjs"
TbHeartFilled, import { useNow } from "hooks/useNow"
TbHelp, import { type ReactNode, useState } from "react"
TbLayoutList, import {
TbList, TbChartLine,
TbListDetails, TbHeartFilled,
TbMoon, TbHelp,
TbNotes, TbLayoutList,
TbPower, TbList,
TbSettings, TbListDetails,
TbSun, TbMoon,
TbSunMoon, TbNotes,
TbUsers, TbPower,
TbWorldDownload, TbSettings,
} from "react-icons/tb" TbSun,
TbSunMoon,
interface ProfileMenuProps { TbUsers,
control: React.ReactElement TbWorldDownload,
} } from "react-icons/tb"
const ProfileMenuControlItem = ({ icon, label }: { icon: ReactNode; label: ReactNode }) => { interface ProfileMenuProps {
return ( control: React.ReactElement
<Group> }
{icon}
<Box ml={6}>{label}</Box> const ProfileMenuControlItem = ({ icon, label }: { icon: ReactNode; label: ReactNode }) => {
</Group> return (
) <Group>
} {icon}
<Box ml={6}>{label}</Box>
const iconSize = 16 </Group>
)
interface ColorSchemeControlItem extends SegmentedControlItem { }
value: MantineColorScheme
} const iconSize = 16
const colorSchemeData: ColorSchemeControlItem[] = [ interface ColorSchemeControlItem extends SegmentedControlItem {
{ value: MantineColorScheme
value: "light", }
label: <ProfileMenuControlItem icon={<TbSun size={iconSize} />} label={<Trans>Light</Trans>} />,
}, const colorSchemeData: ColorSchemeControlItem[] = [
{ {
value: "dark", value: "light",
label: <ProfileMenuControlItem icon={<TbMoon size={iconSize} />} label={<Trans>Dark</Trans>} />, label: <ProfileMenuControlItem icon={<TbSun size={iconSize} />} label={<Trans>Light</Trans>} />,
}, },
{ {
value: "auto", value: "dark",
label: <ProfileMenuControlItem icon={<TbSunMoon size={iconSize} />} label={<Trans>System</Trans>} />, label: <ProfileMenuControlItem icon={<TbMoon size={iconSize} />} label={<Trans>Dark</Trans>} />,
}, },
] {
value: "auto",
interface ViewModeControlItem extends SegmentedControlItem { label: <ProfileMenuControlItem icon={<TbSunMoon size={iconSize} />} label={<Trans>System</Trans>} />,
value: ViewMode },
} ]
const viewModeData: ViewModeControlItem[] = [ interface ViewModeControlItem extends SegmentedControlItem {
{ value: ViewMode
value: "title", }
label: <ProfileMenuControlItem icon={<TbList size={iconSize} />} label={<Trans>Compact</Trans>} />,
}, const viewModeData: ViewModeControlItem[] = [
{ {
value: "cozy", value: "title",
label: <ProfileMenuControlItem icon={<TbLayoutList size={iconSize} />} label={<Trans>Cozy</Trans>} />, label: <ProfileMenuControlItem icon={<TbList size={iconSize} />} label={<Trans>Compact</Trans>} />,
}, },
{ {
value: "detailed", value: "cozy",
label: <ProfileMenuControlItem icon={<TbListDetails size={iconSize} />} label={<Trans>Detailed</Trans>} />, label: <ProfileMenuControlItem icon={<TbLayoutList size={iconSize} />} label={<Trans>Cozy</Trans>} />,
}, },
{ {
value: "expanded", value: "detailed",
label: <ProfileMenuControlItem icon={<TbNotes size={iconSize} />} label={<Trans>Expanded</Trans>} />, label: <ProfileMenuControlItem icon={<TbListDetails size={iconSize} />} label={<Trans>Detailed</Trans>} />,
}, },
] {
value: "expanded",
export function ProfileMenu(props: ProfileMenuProps) { label: <ProfileMenuControlItem icon={<TbNotes size={iconSize} />} label={<Trans>Expanded</Trans>} />,
const [opened, setOpened] = useState(false) },
const { viewMode, setViewMode } = useViewMode() ]
const profile = useAppSelector(state => state.user.profile)
const admin = useAppSelector(state => state.user.profile?.admin) export function ProfileMenu(props: ProfileMenuProps) {
const dispatch = useAppDispatch() const [opened, setOpened] = useState(false)
const { colorScheme, setColorScheme } = useMantineColorScheme() const now = useNow()
const profile = useAppSelector(state => state.user.profile)
const logout = () => { const admin = useAppSelector(state => state.user.profile?.admin)
window.location.href = "logout" const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
} const forceRefreshCooldownDuration = useAppSelector(state => state.server.serverInfos?.forceRefreshCooldownDuration)
const fontSizePercentage = useAppSelector(state => state.user.localSettings.fontSizePercentage)
return ( const dispatch = useAppDispatch()
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}> const { colorScheme, setColorScheme } = useMantineColorScheme()
<Menu.Target>{props.control}</Menu.Target>
<Menu.Dropdown> const nextAvailableForceRefresh = profile?.lastForceRefresh
{profile && <Menu.Label>{profile.name}</Menu.Label>} ? profile.lastForceRefresh + (forceRefreshCooldownDuration ?? 0)
<Menu.Item : now.getTime()
leftSection={<TbSettings size={iconSize} />} const forceRefreshEnabled = nextAvailableForceRefresh <= now.getTime()
onClick={() => {
dispatch(redirectToSettings()) const logout = () => {
setOpened(false) window.location.href = "logout"
}} }
>
<Trans>Settings</Trans> return (
</Menu.Item> <Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}>
<Menu.Item <Menu.Target>{props.control}</Menu.Target>
leftSection={<TbWorldDownload size={iconSize} />} <Menu.Dropdown>
onClick={async () => {profile && <Menu.Label>{profile.name}</Menu.Label>}
await client.feed.refreshAll().then(() => { <Menu.Item
showNotification({ leftSection={<TbSettings size={iconSize} />}
message: <Trans>Your feeds have been queued for refresh.</Trans>, onClick={() => {
color: "green", dispatch(redirectToSettings())
autoClose: 1000, setOpened(false)
}) }}
setOpened(false) >
}) <Trans>Settings</Trans>
} </Menu.Item>
> <Menu.Item
<Trans>Fetch all my feeds now</Trans> leftSection={<TbWorldDownload size={iconSize} />}
</Menu.Item> disabled={!forceRefreshEnabled}
onClick={async () => {
<Divider /> setOpened(false)
<Menu.Label> try {
<Trans>Theme</Trans> await client.feed.refreshAll()
</Menu.Label>
<SegmentedControl // reload profile to update last force refresh timestamp
fullWidth await dispatch(reloadProfile())
orientation="vertical"
data={colorSchemeData} showNotification({
value={colorScheme} message: <Trans>Your feeds have been queued for refresh.</Trans>,
onChange={e => setColorScheme(e as MantineColorScheme)} color: "green",
mb="xs" autoClose: 1000,
/> })
} catch (_) {
<Divider /> showNotification({
message: <Trans>Force fetching feeds is not yet available.</Trans>,
<Menu.Label> color: "red",
<Trans>Display</Trans> autoClose: 2000,
</Menu.Label> })
<SegmentedControl }
fullWidth }}
orientation="vertical" >
data={viewModeData} <Trans>Fetch all my feeds now</Trans>
value={viewMode} {!forceRefreshEnabled && <span> ({dayjs.duration(nextAvailableForceRefresh - now.getTime()).format("HH:mm:ss")})</span>}
onChange={e => setViewMode(e as ViewMode)} </Menu.Item>
mb="xs"
/> <Divider />
{admin && ( <Menu.Label>
<> <Trans>Theme</Trans>
<Divider /> </Menu.Label>
<Menu.Label> <SegmentedControl
<Trans>Admin</Trans> fullWidth
</Menu.Label> orientation="vertical"
<Menu.Item data={colorSchemeData}
leftSection={<TbUsers size={iconSize} />} value={colorScheme}
onClick={() => { onChange={e => setColorScheme(e as MantineColorScheme)}
dispatch(redirectToAdminUsers()) mb="xs"
setOpened(false) />
}}
> <Divider />
<Trans>Manage users</Trans>
</Menu.Item> <Menu.Label>
<Menu.Item <Trans>Display</Trans>
leftSection={<TbChartLine size={iconSize} />} </Menu.Label>
onClick={() => { <SegmentedControl
dispatch(redirectToMetrics()) fullWidth
setOpened(false) orientation="vertical"
}} data={viewModeData}
> value={viewMode}
<Trans>Metrics</Trans> onChange={e => dispatch(setViewMode(e as ViewMode))}
</Menu.Item> mb="xs"
</> />
)}
<Divider />
<Divider />
<Menu.Label>
<Menu.Item <Trans>Font size</Trans>
leftSection={<TbHeartFilled size={iconSize} color="red" />} </Menu.Label>
onClick={() => { <Slider
dispatch(redirectToDonate()) min={50}
setOpened(false) max={150}
}} step={5}
> marks={[{ value: 100 }]}
<Trans>Donate</Trans> label={v => `${v}%`}
</Menu.Item> mb="xs"
value={fontSizePercentage}
<Menu.Item onChange={value => dispatch(setFontSizePercentage(value))}
leftSection={<TbHelp size={iconSize} />} />
onClick={() => {
dispatch(redirectToAbout()) {admin && (
setOpened(false) <>
}} <Divider />
> <Menu.Label>
<Trans>About</Trans> <Trans>Admin</Trans>
</Menu.Item> </Menu.Label>
<Menu.Item leftSection={<TbPower size={iconSize} />} onClick={logout}> <Menu.Item
<Trans>Logout</Trans> leftSection={<TbUsers size={iconSize} />}
</Menu.Item> onClick={() => {
</Menu.Dropdown> dispatch(redirectToAdminUsers())
</Menu> setOpened(false)
) }}
} >
<Trans>Manage users</Trans>
</Menu.Item>
<Menu.Item
leftSection={<TbChartLine size={iconSize} />}
onClick={() => {
dispatch(redirectToMetrics())
setOpened(false)
}}
>
<Trans>Metrics</Trans>
</Menu.Item>
</>
)}
<Divider />
<Menu.Item
leftSection={<TbHeartFilled size={iconSize} color="red" />}
onClick={() => {
dispatch(redirectToDonate())
setOpened(false)
}}
>
<Trans>Donate</Trans>
</Menu.Item>
<Menu.Item
leftSection={<TbHelp size={iconSize} />}
onClick={() => {
dispatch(redirectToAbout())
setOpened(false)
}}
>
<Trans>About</Trans>
</Menu.Item>
<Menu.Item leftSection={<TbPower size={iconSize} />} onClick={logout}>
<Trans>Logout</Trans>
</Menu.Item>
</Menu.Dropdown>
</Menu>
)
}

View File

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

View File

@@ -1,19 +1,19 @@
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: MeterProps) {
return ( return (
<Box> <Box>
<Box>Mean: {props.meter.mean_rate.toFixed(2)}</Box> <Box>Mean: {props.meter.mean_rate.toFixed(2)}</Box>
<Box>Last minute: {props.meter.m1_rate.toFixed(2)}</Box> <Box>Last minute: {props.meter.m1_rate.toFixed(2)}</Box>
<Box>Last 5 minutes: {props.meter.m5_rate.toFixed(2)}</Box> <Box>Last 5 minutes: {props.meter.m5_rate.toFixed(2)}</Box>
<Box>Last 15 minutes: {props.meter.m15_rate.toFixed(2)}</Box> <Box>Last 15 minutes: {props.meter.m15_rate.toFixed(2)}</Box>
<Box>Units: {props.meter.units}</Box> <Box>Units: {props.meter.units}</Box>
<Box>Total: {props.meter.count}</Box> <Box>Total: {props.meter.count}</Box>
</Box> </Box>
) )
} }

View File

@@ -1,22 +1,22 @@
import { Accordion, Box, Group } from "@mantine/core" import { Accordion, Box, Group } from "@mantine/core"
interface MetricAccordionItemProps { interface MetricAccordionItemProps {
metricKey: string metricKey: string
name: string name: string
headerValue: number headerValue: number
children: React.ReactNode children: React.ReactNode
} }
export function MetricAccordionItem({ metricKey, name, headerValue, children }: MetricAccordionItemProps) { export function MetricAccordionItem({ metricKey, name, headerValue, children }: MetricAccordionItemProps) {
return ( return (
<Accordion.Item value={metricKey} key={metricKey}> <Accordion.Item value={metricKey} key={metricKey}>
<Accordion.Control> <Accordion.Control>
<Group justify="space-between"> <Group justify="space-between">
<Box>{name}</Box> <Box>{name}</Box>
<Box>{headerValue}</Box> <Box>{headerValue}</Box>
</Group> </Group>
</Accordion.Control> </Accordion.Control>
<Accordion.Panel>{children}</Accordion.Panel> <Accordion.Panel>{children}</Accordion.Panel>
</Accordion.Item> </Accordion.Item>
) )
} }

View File

@@ -1,19 +1,19 @@
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: MetricTimerProps) {
return ( return (
<Box> <Box>
<Box>Mean: {props.timer.mean_rate.toFixed(2)}</Box> <Box>Mean: {props.timer.mean_rate.toFixed(2)}</Box>
<Box>Last minute: {props.timer.m1_rate.toFixed(2)}</Box> <Box>Last minute: {props.timer.m1_rate.toFixed(2)}</Box>
<Box>Last 5 minutes: {props.timer.m5_rate.toFixed(2)}</Box> <Box>Last 5 minutes: {props.timer.m5_rate.toFixed(2)}</Box>
<Box>Last 15 minutes: {props.timer.m15_rate.toFixed(2)}</Box> <Box>Last 15 minutes: {props.timer.m15_rate.toFixed(2)}</Box>
<Box>Units: {props.timer.rate_units}</Box> <Box>Units: {props.timer.rate_units}</Box>
<Box>Total: {props.timer.count}</Box> <Box>Total: {props.timer.count}</Box>
</Box> </Box>
) )
} }

View File

@@ -1,8 +1,8 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
import React from "react" import type React from "react"
export function OnDesktop(props: { children: React.ReactNode }) { export function OnDesktop(props: { children: React.ReactNode }) {
const mobile = useMobile() const mobile = useMobile()
return <Box>{!mobile && props.children}</Box> return <Box>{!mobile && props.children}</Box>
} }

View File

@@ -1,8 +1,8 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
import React from "react" import type React from "react"
export function OnMobile(props: { children: React.ReactNode }) { export function OnMobile(props: { children: React.ReactNode }) {
const mobile = useMobile() const mobile = useMobile()
return <Box>{mobile && props.children}</Box> return <Box>{mobile && props.children}</Box>
} }

View File

@@ -1,7 +1,8 @@
import { Trans } from "@lingui/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 { client, errorToStrings } from "app/client"
import { Constants } from "app/constants"
import { redirectToSelectedSource } from "app/redirect/thunks" import { redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
@@ -57,13 +58,27 @@ 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={
<Trans>
<span>See </span>
<Anchor
href={Constants.customCssDocumentationUrl}
target="_blank"
rel="noreferrer"
style={{ fontSize: "inherit" }}
>
here
</Anchor>
<span> for more information.</span>
</Trans>
}
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

@@ -1,161 +1,227 @@
import { t, Trans } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core" import { useLingui } from "@lingui/react"
import { type ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types" import { Trans } from "@lingui/react/macro"
import { Constants } from "app/constants" import { Box, Divider, Group, NumberInput, Radio, Select, type SelectProps, SimpleGrid, Stack, Switch } from "@mantine/core"
import { useAppDispatch, useAppSelector } from "app/store" import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { type IconDisplayMode, type ScrollMode, type SharingSettings } from "app/types" import { Constants } from "app/constants"
import { import { useAppDispatch, useAppSelector } from "app/store"
changeCustomContextMenu, import type { IconDisplayMode, ScrollMode, SharingSettings } from "app/types"
changeExternalLinkIconDisplayMode, import {
changeLanguage, changeCustomContextMenu,
changeMarkAllAsReadConfirmation, changeEntriesToKeepOnTopWhenScrolling,
changeMobileFooter, changeExternalLinkIconDisplayMode,
changeScrollMarks, changeLanguage,
changeScrollMode, changeMarkAllAsReadConfirmation,
changeScrollSpeed, changeMobileFooter,
changeSharingSetting, changePrimaryColor,
changeShowRead, changeScrollMarks,
changeStarIconDisplayMode, changeScrollMode,
} from "app/user/thunks" changeScrollSpeed,
import { locales } from "i18n" changeSharingSetting,
import { type ReactNode } from "react" changeShowRead,
changeStarIconDisplayMode,
export function DisplaySettings() { changeUnreadCountFavicon,
const language = useAppSelector(state => state.user.settings?.language) changeUnreadCountTitle,
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed) } from "app/user/thunks"
const showRead = useAppSelector(state => state.user.settings?.showRead) import { locales } from "i18n"
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) import type { ReactNode } from "react"
const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode) export function DisplaySettings() {
const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode) const language = useAppSelector(state => state.user.settings?.language)
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation) const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu) const showRead = useAppSelector(state => state.user.settings?.showRead)
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
const dispatch = useAppDispatch() const entriesToKeepOnTop = useAppSelector(state => state.user.settings?.entriesToKeepOnTopWhenScrolling)
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
const scrollModeOptions: Record<ScrollMode, ReactNode> = { const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
always: <Trans>Always</Trans>, const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
never: <Trans>Never</Trans>, const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
if_needed: <Trans>If the entry doesn't entirely fit on the screen</Trans>, const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
} const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const displayModeData: ComboboxData = [ const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
{ const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
value: "always", const { _ } = useLingui()
label: t`Always`, const dispatch = useAppDispatch()
},
{ const scrollModeOptions: Record<ScrollMode, ReactNode> = {
value: "on_desktop", always: <Trans>Always</Trans>,
label: t`On desktop`, never: <Trans>Never</Trans>,
}, if_needed: <Trans>If the entry doesn't entirely fit on the screen</Trans>,
{ }
value: "on_mobile",
label: t`On mobile`, const displayModeData: ComboboxData = [
}, {
{ value: "always",
value: "never", label: _(msg`Always`),
label: t`Never`, },
}, {
] value: "on_desktop",
label: _(msg`On desktop`),
return ( },
<Stack> {
<Select value: "on_mobile",
description={<Trans>Language</Trans>} label: _(msg`On mobile`),
value={language} },
data={locales.map(l => ({ {
value: l.key, value: "never",
label: l.label, label: _(msg`Never`),
}))} },
onChange={async s => await (s && dispatch(changeLanguage(s)))} ]
/>
const colorData: ComboboxData = [
<Switch { value: "dark", label: _(msg`Dark`) },
label={<Trans>Show feeds and categories with no unread entries</Trans>} { value: "gray", label: _(msg`Gray`) },
checked={showRead} { value: "red", label: _(msg`Red`) },
onChange={async e => await dispatch(changeShowRead(e.currentTarget.checked))} { value: "pink", label: _(msg`Pink`) },
/> { value: "grape", label: _(msg`Grape`) },
{ value: "violet", label: _(msg`Violet`) },
<Switch { value: "indigo", label: _(msg`Indigo`) },
label={<Trans>Show confirmation when marking all entries as read</Trans>} { value: "blue", label: _(msg`Blue`) },
checked={markAllAsReadConfirmation} { value: "cyan", label: _(msg`Cyan`) },
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))} { value: "green", label: _(msg`Green`) },
/> { value: "lime", label: _(msg`Lime`) },
{ value: "yellow", label: _(msg`Yellow`) },
<Switch { value: "orange", label: _(msg`Orange`) },
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>} { value: "teal", label: _(msg`Teal`) },
checked={mobileFooter} ].sort((a, b) => a.label.localeCompare(b.label))
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))} const colorRenderer: SelectProps["renderOption"] = ({ option }) => (
/> <Group>
<Box h={18} w={18} bg={option.value} />
<Divider label={<Trans>Entry headers</Trans>} labelPosition="center" /> <Box>{option.label}</Box>
</Group>
<Select )
description={<Trans>Show star icon</Trans>}
value={starIconDisplayMode} return (
data={displayModeData} <Stack>
onChange={async s => await dispatch(changeStarIconDisplayMode(s as IconDisplayMode))} <Divider label={<Trans>Display</Trans>} labelPosition="center" />
/>
<Select
<Select label={<Trans>Language</Trans>}
description={<Trans>Show external link icon</Trans>} value={language}
value={externalLinkIconDisplayMode} data={locales.map(l => ({
data={displayModeData} value: l.key,
onChange={async s => await dispatch(changeExternalLinkIconDisplayMode(s as IconDisplayMode))} label: l.label,
/> }))}
onChange={async s => await (s && dispatch(changeLanguage(s)))}
<Switch />
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
checked={customContextMenu} <Select
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))} label={<Trans>Primary color</Trans>}
/> data={colorData}
value={primaryColor}
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" /> onChange={async value => value && (await dispatch(changePrimaryColor(value)))}
renderOption={colorRenderer}
<Radio.Group />
label={<Trans>Scroll selected entry to the top of the page</Trans>}
value={scrollMode} <Switch
onChange={async value => await dispatch(changeScrollMode(value as ScrollMode))} label={<Trans>Show feeds and categories with no unread entries</Trans>}
> checked={showRead}
<Group mt="xs"> onChange={async e => await dispatch(changeShowRead(e.currentTarget.checked))}
{Object.entries(scrollModeOptions).map(e => ( />
<Radio key={e[0]} value={e[0]} label={e[1]} />
))} <Switch
</Group> label={<Trans>Show confirmation when marking all entries as read</Trans>}
</Radio.Group> checked={markAllAsReadConfirmation}
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
<Switch />
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
checked={scrollSpeed ? scrollSpeed > 0 : false} <Switch
onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))} label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
/> checked={mobileFooter}
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
<Switch />
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
checked={scrollMarks} <Divider label={<Trans>Browser tab</Trans>} labelPosition="center" />
onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
/> <Switch
label={<Trans>Show unread count in tab title</Trans>}
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" /> checked={unreadCountTitle}
onChange={async e => await dispatch(changeUnreadCountTitle(e.currentTarget.checked))}
<SimpleGrid cols={2}> />
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>).map(site => (
<Switch <Switch
key={site} label={<Trans>Show unread count in tab favicon</Trans>}
label={Constants.sharing[site].label} checked={unreadCountFavicon}
checked={sharingSettings?.[site]} onChange={async e => await dispatch(changeUnreadCountFavicon(e.currentTarget.checked))}
onChange={async e => />
await dispatch(
changeSharingSetting({ <Divider label={<Trans>Entry headers</Trans>} labelPosition="center" />
site,
value: e.currentTarget.checked, <Select
}) label={<Trans>Show star icon</Trans>}
) value={starIconDisplayMode}
} data={displayModeData}
/> onChange={async s => await dispatch(changeStarIconDisplayMode(s as IconDisplayMode))}
))} />
</SimpleGrid>
</Stack> <Select
) label={<Trans>Show external link icon</Trans>}
} value={externalLinkIconDisplayMode}
data={displayModeData}
onChange={async s => await dispatch(changeExternalLinkIconDisplayMode(s as IconDisplayMode))}
/>
<Switch
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
checked={customContextMenu}
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
/>
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
<Radio.Group
label={<Trans>Scroll selected entry to the top of the page</Trans>}
value={scrollMode}
onChange={async value => await dispatch(changeScrollMode(value as ScrollMode))}
>
<Group mt="xs">
{Object.entries(scrollModeOptions).map(e => (
<Radio key={e[0]} value={e[0]} label={e[1]} />
))}
</Group>
</Radio.Group>
<NumberInput
label={<Trans>Entries to keep above the selected entry when scrolling</Trans>}
description={<Trans>Only applies to compact, cozy and detailed modes</Trans>}
min={0}
value={entriesToKeepOnTop}
onChange={async value => await dispatch(changeEntriesToKeepOnTopWhenScrolling(+value))}
/>
<Switch
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
checked={scrollSpeed ? scrollSpeed > 0 : false}
onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))}
/>
<Switch
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
checked={scrollMarks}
onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
/>
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
<SimpleGrid cols={2}>
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>).map(site => (
<Switch
key={site}
label={Constants.sharing[site].label}
checked={sharingSettings?.[site]}
onChange={async e =>
await dispatch(
changeSharingSetting({
site,
value: e.currentTarget.checked,
})
)
}
/>
))}
</SimpleGrid>
</Stack>
)
}

View File

@@ -1,162 +1,165 @@
import { t, Trans } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core" import { useLingui } from "@lingui/react"
import { useForm } from "@mantine/form" import { Trans } from "@lingui/react/macro"
import { openConfirmModal } from "@mantine/modals" import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
import { client, errorToStrings } from "app/client" import { useForm } from "@mantine/form"
import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks" import { openConfirmModal } from "@mantine/modals"
import { useAppDispatch, useAppSelector } from "app/store" import { client, errorToStrings } from "app/client"
import { type ProfileModificationRequest } from "app/types" import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
import { reloadProfile } from "app/user/thunks" import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert" import type { ProfileModificationRequest } from "app/types"
import { useEffect } from "react" import { reloadProfile } from "app/user/thunks"
import { useAsyncCallback } from "react-async-hook" import { Alert } from "components/Alert"
import { TbDeviceFloppy, TbTrash } from "react-icons/tb" import { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook"
interface FormData extends ProfileModificationRequest { import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
newPasswordConfirmation?: string
} interface FormData extends ProfileModificationRequest {
newPasswordConfirmation?: string
export function ProfileSettings() { }
const profile = useAppSelector(state => state.user.profile)
const dispatch = useAppDispatch() export function ProfileSettings() {
const profile = useAppSelector(state => state.user.profile)
const form = useForm<FormData>({ const dispatch = useAppDispatch()
validate: { const { _ } = useLingui()
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? t`Passwords do not match` : null),
}, const form = useForm<FormData>({
}) validate: {
const { setValues } = form newPasswordConfirmation: (value, values) => (value !== values.newPassword ? _(msg`Passwords do not match`) : null),
},
const saveProfile = useAsyncCallback(client.user.saveProfile, { })
onSuccess: () => { const { setValues } = form
dispatch(reloadProfile())
dispatch(redirectToSelectedSource()) const saveProfile = useAsyncCallback(client.user.saveProfile, {
}, onSuccess: () => {
}) dispatch(reloadProfile())
const deleteProfile = useAsyncCallback(client.user.deleteProfile, { dispatch(redirectToSelectedSource())
onSuccess: () => { },
dispatch(redirectToLogin()) })
}, const deleteProfile = useAsyncCallback(client.user.deleteProfile, {
}) onSuccess: () => {
dispatch(redirectToLogin())
const openDeleteProfileModal = () => },
openConfirmModal({ })
title: <Trans>Delete account</Trans>,
children: ( const openDeleteProfileModal = () =>
<Text size="sm"> openConfirmModal({
<Trans>Are you sure you want to delete your account? There's no turning back!</Trans> title: <Trans>Delete account</Trans>,
</Text> children: (
), <Text size="sm">
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> }, <Trans>Are you sure you want to delete your account? There's no turning back!</Trans>
confirmProps: { color: "red" }, </Text>
onConfirm: async () => await deleteProfile.execute(), ),
}) labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
confirmProps: { color: "red" },
useEffect(() => { onConfirm: async () => await deleteProfile.execute(),
if (!profile) return })
setValues({
currentPassword: "", useEffect(() => {
email: profile.email ?? "", if (!profile) return
newApiKey: false, setValues({
}) currentPassword: "",
}, [setValues, profile]) email: profile.email ?? "",
newApiKey: false,
return ( })
<> }, [setValues, profile])
{saveProfile.error && (
<Box mb="md"> return (
<Alert messages={errorToStrings(saveProfile.error)} /> <>
</Box> {saveProfile.error && (
)} <Box mb="md">
<Alert messages={errorToStrings(saveProfile.error)} />
{deleteProfile.error && ( </Box>
<Box mb="md"> )}
<Alert messages={errorToStrings(deleteProfile.error)} />
</Box> {deleteProfile.error && (
)} <Box mb="md">
<Alert messages={errorToStrings(deleteProfile.error)} />
<form onSubmit={form.onSubmit(saveProfile.execute)}> </Box>
<Stack> )}
<TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
<TextInput <form onSubmit={form.onSubmit(saveProfile.execute)}>
label={<Trans>API key</Trans>} <Stack>
description={ <TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
<Trans> <TextInput
This is your API key. It can be used for some read-only API operations and grants access to the Fever API. label={<Trans>API key</Trans>}
Use the form at the bottom of the page to generate a new API key description={
</Trans> <Trans>
} This is your API key. It can be used for some read-only API operations and grants access to the Fever API.
readOnly Use the form at the bottom of the page to generate a new API key
value={profile?.apiKey} </Trans>
/> }
readOnly
<Input.Wrapper value={profile?.apiKey}
label={<Trans>OPML export</Trans>} />
description={
<Trans> <Input.Wrapper
Export your subscriptions and categories as an OPML file that can be imported in other feed reading services label={<Trans>OPML export</Trans>}
</Trans> description={
} <Trans>
> Export your subscriptions and categories as an OPML file that can be imported in other feed reading services
<Box> </Trans>
<Anchor href="rest/feed/export" download="commafeed.opml"> }
<Trans>Download</Trans> >
</Anchor> <Box>
</Box> <Anchor href="rest/feed/export" download="commafeed.opml">
</Input.Wrapper> <Trans>Download</Trans>
</Anchor>
<Input.Wrapper </Box>
label={<Trans>Fever API</Trans>} </Input.Wrapper>
description={
<Trans> <Input.Wrapper
CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. label={<Trans>Fever API</Trans>}
Login with your username and your <u>API key</u>. description={
</Trans> <Trans>
} CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client.
> Login with your username and your <u>API key</u>.
<Box> </Trans>
<Anchor href={`rest/fever/user/${profile?.id}`} target="_blank"> }
<Trans>Fever API URL</Trans> >
</Anchor> <Box>
</Box> <Anchor href={`rest/fever/user/${profile?.id}`} target="_blank">
</Input.Wrapper> <Trans>Fever API URL</Trans>
</Anchor>
<Divider /> </Box>
</Input.Wrapper>
<PasswordInput
label={<Trans>Current password</Trans>} <Divider />
description={<Trans>Enter your current password to change profile settings</Trans>}
required <PasswordInput
{...form.getInputProps("currentPassword")} label={<Trans>Current password</Trans>}
/> description={<Trans>Enter your current password to change profile settings</Trans>}
<TextInput type="email" label={<Trans>E-mail</Trans>} {...form.getInputProps("email")} required /> required
<PasswordInput {...form.getInputProps("currentPassword")}
label={<Trans>New password</Trans>} />
description={<Trans>Changing password will generate a new API key</Trans>} <TextInput type="email" label={<Trans>E-mail</Trans>} {...form.getInputProps("email")} required />
{...form.getInputProps("newPassword")} <PasswordInput
/> label={<Trans>New password</Trans>}
<PasswordInput label={<Trans>Confirm password</Trans>} {...form.getInputProps("newPasswordConfirmation")} /> description={<Trans>Changing password will generate a new API key</Trans>}
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} /> {...form.getInputProps("newPassword")}
/>
<Group> <PasswordInput label={<Trans>Confirm password</Trans>} {...form.getInputProps("newPasswordConfirmation")} />
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}> <Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
<Trans>Cancel</Trans>
</Button> <Group>
<Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Save</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Divider orientation="vertical" /> <Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>
<Button <Trans>Save</Trans>
color="red" </Button>
leftSection={<TbTrash size={16} />} <Divider orientation="vertical" />
onClick={() => openDeleteProfileModal()} <Button
loading={deleteProfile.loading} color="red"
> leftSection={<TbTrash size={16} />}
<Trans>Delete account</Trans> onClick={() => openDeleteProfileModal()}
</Button> loading={deleteProfile.loading}
</Group> >
</Stack> <Trans>Delete account</Trans>
</form> </Button>
</> </Group>
) </Stack>
} </form>
</>
)
}

View File

@@ -1,175 +1,200 @@
import { Trans } from "@lingui/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 { Constants } from "app/constants"
import { import {
redirectToCategory, redirectToCategory,
redirectToCategoryDetails, redirectToCategoryDetails,
redirectToFeed, redirectToFeed,
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, type 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 React from "react"
import { TreeNode } from "./TreeNode" import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
import { TreeSearch } from "./TreeSearch" import { TreeNode } from "./TreeNode"
import { TreeSearch } from "./TreeSearch"
const allIcon = <TbInbox size={16} />
const starredIcon = <TbStar size={16} /> const allIcon = <TbInbox size={16} />
const tagIcon = <TbTag size={16} /> const starredIcon = <TbStar size={16} />
const expandedIcon = <TbChevronDown size={16} /> const tagIcon = <TbTag size={16} />
const collapsedIcon = <TbChevronRight size={16} /> const expandedIcon = <TbChevronDown size={16} />
const collapsedIcon = <TbChevronRight size={16} />
const errorThreshold = 9
const errorThreshold = 9
export function Tree() {
const root = useAppSelector(state => state.tree.rootCategory) export function Tree() {
const source = useAppSelector(state => state.entries.source) const root = useAppSelector(state => state.tree.rootCategory)
const tags = useAppSelector(state => state.user.tags) const source = useAppSelector(state => state.entries.source)
const showRead = useAppSelector(state => state.user.settings?.showRead) const tags = useAppSelector(state => state.user.tags)
const dispatch = useAppDispatch() const showRead = useAppSelector(state => state.user.settings?.showRead)
const dispatch = useAppDispatch()
const feedClicked = (e: React.MouseEvent, id: string) => {
if (e.detail === 2) { const isFeedDisplayed = (feed: Subscription) => {
dispatch(redirectToFeedDetails(id)) const isCurrentFeed = source.type === "feed" && source.id === String(feed.id)
} else { return isCurrentFeed || feed.unread > 0 || showRead
dispatch(redirectToFeed(id)) }
}
} const isCategoryDisplayed = (category: Category): boolean => {
const categoryClicked = (e: React.MouseEvent, id: string) => { const isCurrentCategory = source.type === "category" && source.id === category.id
if (e.detail === 2) { return (
dispatch(redirectToCategoryDetails(id)) isCurrentCategory ||
} else { showRead ||
dispatch(redirectToCategory(id)) category.children.some(c => isCategoryDisplayed(c)) ||
} category.feeds.some(f => isFeedDisplayed(f))
} )
const categoryIconClicked = (e: React.MouseEvent, category: Category) => { }
e.stopPropagation()
const feedClicked = (e: React.MouseEvent, id: string) => {
dispatch( if (e.detail === 2) {
collapseTreeCategory({ dispatch(redirectToFeedDetails(id))
id: +category.id, } else {
collapse: category.expanded, dispatch(redirectToFeed(id))
}) }
) }
} const categoryClicked = (e: React.MouseEvent, id: string) => {
const tagClicked = (e: React.MouseEvent, id: string) => { if (e.detail === 2) {
if (e.detail === 2) { dispatch(redirectToCategoryDetails(id))
dispatch(redirectToTagDetails(id)) } else {
} else { dispatch(redirectToCategory(id))
dispatch(redirectToTag(id)) }
} }
} const categoryIconClicked = (e: React.MouseEvent, category: Category) => {
e.stopPropagation()
const allCategoryNode = () => (
<TreeNode dispatch(
id={Constants.categories.all.id} collapseTreeCategory({
name={<Trans>All</Trans>} id: +category.id,
icon={allIcon} collapse: category.expanded,
unread={categoryUnreadCount(root)} })
selected={source.type === "category" && source.id === Constants.categories.all.id} )
expanded={false} }
level={0} const tagClicked = (e: React.MouseEvent, id: string) => {
hasError={false} if (e.detail === 2) {
onClick={categoryClicked} dispatch(redirectToTagDetails(id))
/> } else {
) dispatch(redirectToTag(id))
const starredCategoryNode = () => ( }
<TreeNode }
id={Constants.categories.starred.id}
name={<Trans>Starred</Trans>} const allCategoryNode = () => (
icon={starredIcon} <TreeNode
unread={0} id={Constants.categories.all.id}
selected={source.type === "category" && source.id === Constants.categories.starred.id} type="category"
expanded={false} name={<Trans>All</Trans>}
level={0} icon={allIcon}
hasError={false} unread={categoryUnreadCount(root)}
onClick={categoryClicked} hasNewEntries={categoryHasNewEntries(root)}
/> selected={source.type === "category" && source.id === Constants.categories.all.id}
) expanded={false}
level={0}
const categoryNode = (category: Category, level = 0) => { hasError={false}
const unreadCount = categoryUnreadCount(category) onClick={categoryClicked}
if (unreadCount === 0 && !showRead) return null />
)
const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold)) const starredCategoryNode = () => (
return ( <TreeNode
<TreeNode id={Constants.categories.starred.id}
id={category.id} type="category"
name={category.name} name={<Trans>Starred</Trans>}
icon={category.expanded ? expandedIcon : collapsedIcon} icon={starredIcon}
unread={unreadCount} unread={0}
selected={source.type === "category" && source.id === category.id} hasNewEntries={false}
expanded={category.expanded} selected={source.type === "category" && source.id === Constants.categories.starred.id}
level={level} expanded={false}
hasError={hasError} level={0}
onClick={categoryClicked} hasError={false}
onIconClick={e => categoryIconClicked(e, category)} onClick={categoryClicked}
key={category.id} />
/> )
)
} const categoryNode = (category: Category, level = 0) => {
if (!isCategoryDisplayed(category)) return null
const feedNode = (feed: Subscription, level = 0) => {
if (feed.unread === 0 && !showRead) return null const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
return (
return ( <TreeNode
<TreeNode id={category.id}
id={String(feed.id)} type="category"
name={feed.name} name={category.name}
icon={feed.iconUrl} icon={category.expanded ? expandedIcon : collapsedIcon}
unread={feed.unread} unread={categoryUnreadCount(category)}
selected={source.type === "feed" && source.id === String(feed.id)} hasNewEntries={categoryHasNewEntries(category)}
level={level} selected={source.type === "category" && source.id === category.id}
hasError={feed.errorCount > errorThreshold} expanded={category.expanded}
onClick={feedClicked} level={level}
key={feed.id} hasError={hasError}
/> onClick={categoryClicked}
) onIconClick={e => categoryIconClicked(e, category)}
} key={category.id}
/>
const tagNode = (tag: string) => ( )
<TreeNode }
id={tag}
name={tag} const feedNode = (feed: TreeSubscription, level = 0) => {
icon={tagIcon} if (!isFeedDisplayed(feed)) return null
unread={0}
selected={source.type === "tag" && source.id === tag} return (
level={0} <TreeNode
hasError={false} id={String(feed.id)}
onClick={tagClicked} type="feed"
key={tag} name={feed.name}
/> icon={feed.iconUrl}
) unread={feed.unread}
hasNewEntries={!!feed.hasNewEntries}
const recursiveCategoryNode = (category: Category, level = 0) => ( selected={source.type === "feed" && source.id === String(feed.id)}
<React.Fragment key={`recursiveCategoryNode-${category.id}`}> level={level}
{categoryNode(category, level)} hasError={feed.errorCount > errorThreshold}
{category.expanded && category.children.map(c => recursiveCategoryNode(c, level + 1))} onClick={feedClicked}
{category.expanded && category.feeds.map(f => feedNode(f, level + 1))} key={feed.id}
</React.Fragment> />
) )
}
if (!root) return <Loader />
const feeds = flattenCategoryTree(root).flatMap(c => c.feeds) const tagNode = (tag: string) => (
return ( <TreeNode
<Stack> id={tag}
<OnDesktop> type="tag"
<TreeSearch feeds={feeds} /> name={tag}
</OnDesktop> icon={tagIcon}
<Box> unread={0}
{allCategoryNode()} hasNewEntries={false}
{starredCategoryNode()} selected={source.type === "tag" && source.id === tag}
{root.children.map(c => recursiveCategoryNode(c))} level={0}
{root.feeds.map(f => feedNode(f))} hasError={false}
{tags?.map(tag => tagNode(tag))} onClick={tagClicked}
</Box> key={tag}
</Stack> />
) )
}
const recursiveCategoryNode = (category: Category, level = 0) => (
<React.Fragment key={`recursiveCategoryNode-${category.id}`}>
{categoryNode(category, level)}
{category.expanded && category.children.map(c => recursiveCategoryNode(c, level + 1))}
{category.expanded && category.feeds.map(f => feedNode(f, level + 1))}
</React.Fragment>
)
if (!root) return <Loader />
const feeds = flattenCategoryTree(root).flatMap(c => c.feeds)
return (
<Stack>
<OnDesktop>
<TreeSearch feeds={feeds} />
</OnDesktop>
<Box className="cf-tree">
{allCategoryNode()}
{starredCategoryNode()}
{root.children.map(c => recursiveCategoryNode(c))}
{root.feeds.map(f => feedNode(f))}
{tags?.map(tag => tagNode(tag))}
</Box>
</Stack>
)
}

View File

@@ -1,78 +1,89 @@
import { Box, Center } from "@mantine/core" import { Box, Center } from "@mantine/core"
import { FeedFavicon } from "components/content/FeedFavicon" import type { EntrySourceType } from "app/entries/slice"
import React, { type ReactNode } from "react" import { FeedFavicon } from "components/content/FeedFavicon"
import { tss } from "tss" import type React from "react"
import { UnreadCount } from "./UnreadCount" import { tss } from "tss"
import { UnreadCount } from "./UnreadCount"
interface TreeNodeProps {
id: string interface TreeNodeProps {
name: ReactNode id: string
icon: ReactNode type: EntrySourceType
unread: number name: React.ReactNode
selected: boolean icon: React.ReactNode
expanded?: boolean unread: number
level: number selected: boolean
hasError: boolean expanded?: boolean
onClick: (e: React.MouseEvent, id: string) => void level: number
onIconClick?: (e: React.MouseEvent, id: string) => void hasError: boolean
} hasNewEntries: boolean
onClick: (e: React.MouseEvent, id: string) => void
const useStyles = tss onIconClick?: (e: React.MouseEvent, id: string) => void
.withParams<{ }
selected: boolean
hasError: boolean const useStyles = tss
hasUnread: boolean .withParams<{
}>() selected: boolean
.create(({ theme, colorScheme, selected, hasError, hasUnread }) => { hasError: boolean
let backgroundColor = "inherit" hasUnread: boolean
if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1] }>()
.create(({ theme, colorScheme, selected, hasError, hasUnread }) => {
let color let backgroundColor = "inherit"
if (hasError) { if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]
color = theme.colors.red[6]
} else if (colorScheme === "dark") { let color: string
color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3] if (hasError) {
} else { color = theme.colors.red[6]
color = hasUnread ? theme.black : theme.colors.gray[6] } else if (colorScheme === "dark") {
} color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3]
} else {
return { color = hasUnread ? theme.black : theme.colors.gray[6]
node: { }
display: "flex",
alignItems: "center", return {
cursor: "pointer", node: {
color, display: "flex",
backgroundColor, alignItems: "center",
"&:hover": { cursor: "pointer",
backgroundColor: colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0], color,
}, backgroundColor,
}, "&:hover": {
nodeText: { backgroundColor: colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0],
flexGrow: 1, },
whiteSpace: "nowrap", },
overflow: "hidden", nodeText: {
textOverflow: "ellipsis", flexGrow: 1,
}, whiteSpace: "nowrap",
} overflow: "hidden",
}) textOverflow: "ellipsis",
},
export function TreeNode(props: TreeNodeProps) { }
const { classes } = useStyles({ })
selected: props.selected,
hasError: props.hasError, export function TreeNode(props: TreeNodeProps) {
hasUnread: props.unread > 0, const { classes } = useStyles({
}) selected: props.selected,
return ( hasError: props.hasError,
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}> hasUnread: props.unread > 0,
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}> })
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center> return (
</Box> <Box
<Box className={classes.nodeText}>{props.name}</Box> py={1}
{!props.expanded && ( pl={props.level * 20}
<Box> className={`${classes.node} cf-treenode cf-treenode-${props.type}`}
<UnreadCount unreadCount={props.unread} /> onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
</Box> data-id={props.id}
)} data-type={props.type}
</Box> data-unread-count={props.unread}
) >
} <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>
</Box>
<Box className={classes.nodeText}>{props.name}</Box>
{!props.expanded && (
<Box className="cf-treenode-unread-count">
<UnreadCount unreadCount={props.unread} showIndicator={props.hasNewEntries} />
</Box>
)}
</Box>
)
}

View File

@@ -1,68 +1,63 @@
import { t, Trans } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { Box, Center, Kbd, TextInput } from "@mantine/core" import { useLingui } from "@lingui/react"
import { Spotlight, spotlight, type SpotlightActionData } from "@mantine/spotlight" import { Trans } from "@lingui/react/macro"
import { redirectToFeed } from "app/redirect/thunks" import { Box, TextInput } from "@mantine/core"
import { useAppDispatch } from "app/store" import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
import { type Subscription } from "app/types" import { redirectToFeed } from "app/redirect/thunks"
import { FeedFavicon } from "components/content/FeedFavicon" import { useAppDispatch } from "app/store"
import { useMousetrap } from "hooks/useMousetrap" import type { Subscription } from "app/types"
import { TbSearch } from "react-icons/tb" import { FeedFavicon } from "components/content/FeedFavicon"
import { useMousetrap } from "hooks/useMousetrap"
export interface TreeSearchProps { import { TbSearch } from "react-icons/tb"
feeds: Subscription[]
} export interface TreeSearchProps {
feeds: Subscription[]
export function TreeSearch(props: TreeSearchProps) { }
const dispatch = useAppDispatch()
export function TreeSearch(props: TreeSearchProps) {
const actions: SpotlightActionData[] = props.feeds const dispatch = useAppDispatch()
.map(f => ({ const { _ } = useLingui()
id: `${f.id}`,
label: f.name, const actions: SpotlightActionData[] = props.feeds
leftSection: <FeedFavicon url={f.iconUrl} />, .map(f => ({
onClick: async () => await dispatch(redirectToFeed(f.id)), id: `${f.id}`,
})) label: f.name,
.sort((f1, f2) => f1.label.localeCompare(f2.label)) leftSection: <FeedFavicon url={f.iconUrl} />,
onClick: async () => await dispatch(redirectToFeed(f.id)),
const searchIcon = <TbSearch size={18} /> }))
const rightSection = ( .sort((f1, f2) => f1.label.localeCompare(f2.label))
<Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
<Kbd>Ctrl</Kbd> const searchIcon = <TbSearch size={18} />
<Box mx={5}>+</Box>
<Kbd>K</Kbd> // additional keyboard shortcut used by commafeed v1
</Center> useMousetrap("g u", () => spotlight.open())
)
return (
// additional keyboard shortcut used by commafeed v1 <Box className="cf-treesearch">
useMousetrap("g u", () => spotlight.open()) <TextInput
placeholder={_(msg`Search`)}
return ( leftSection={searchIcon}
<> rightSectionWidth={100}
<TextInput styles={{
placeholder={t`Search`} input: {
leftSection={searchIcon} cursor: "pointer",
rightSectionWidth={100} },
rightSection={rightSection} }}
styles={{ onClick={() => spotlight.open()}
input: { // prevent focus
cursor: "pointer", onFocus={e => e.target.blur()}
}, readOnly
}} />
onClick={() => spotlight.open()} <Spotlight
// prevent focus actions={actions}
onFocus={e => e.target.blur()} limit={10}
readOnly shortcut="mod+k"
/> searchProps={{
<Spotlight leftSection: searchIcon,
actions={actions} placeholder: _(msg`Search`),
limit={10} }}
shortcut="ctrl+k" nothingFound={<Trans>Nothing found</Trans>}
searchProps={{ />
leftSection: searchIcon, </Box>
placeholder: t`Search`, )
}} }
nothingFound={<Trans>Nothing found</Trans>}
></Spotlight>
</>
)
}

View File

@@ -1,26 +1,28 @@
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: {
width: "3.2rem", width: "3.2rem",
// for some reason, mantine Badge has "cursor: 'default'" // for some reason, mantine Badge has "cursor: 'default'"
cursor: "pointer", cursor: "pointer",
}, },
})) }))
export function UnreadCount(props: { unreadCount: number }) { export function UnreadCount(props: { unreadCount: number; showIndicator: boolean }) {
const { classes } = useStyles() const { classes } = useStyles()
if (props.unreadCount <= 0) return null if (props.unreadCount <= 0) return null
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"> <Indicator disabled={!props.showIndicator} size={4} offset={10} position="middle-start">
{count} <Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
</Badge> {count}
</Tooltip> </Badge>
) </Indicator>
} </Tooltip>
)
}

View File

@@ -1,9 +1,9 @@
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()
const mobile = useMobile(theme.breakpoints.xl) const mobile = useMobile(theme.breakpoints.xl)
const spacing = mobile ? 14 : 0 const spacing = mobile ? 14 : 0
return { mobile, spacing } return { mobile, spacing }
} }

View File

@@ -1,39 +1,41 @@
import { t } from "@lingui/macro" import { msg } from "@lingui/core/macro"
import { useAppSelector } from "app/store" import { useLingui } from "@lingui/react"
import { useAppSelector } from "app/store"
interface Step {
label: string interface Step {
done: boolean label: string
} done: boolean
}
export const useAppLoading = () => {
const profile = useAppSelector(state => state.user.profile) export const useAppLoading = () => {
const settings = useAppSelector(state => state.user.settings) const profileLoaded = useAppSelector(state => !!state.user.profile)
const rootCategory = useAppSelector(state => state.tree.rootCategory) const settingsLoaded = useAppSelector(state => !!state.user.settings)
const tags = useAppSelector(state => state.user.tags) const rootCategoryLoaded = useAppSelector(state => !!state.tree.rootCategory)
const tagsLoaded = useAppSelector(state => !!state.user.tags)
const steps: Step[] = [ const { _ } = useLingui()
{
label: t`Loading settings...`, const steps: Step[] = [
done: !!settings, {
}, label: _(msg`Loading settings...`),
{ done: settingsLoaded,
label: t`Loading profile...`, },
done: !!profile, {
}, label: _(msg`Loading profile...`),
{ done: profileLoaded,
label: t`Loading subscriptions...`, },
done: !!rootCategory, {
}, label: _(msg`Loading subscriptions...`),
{ done: rootCategoryLoaded,
label: t`Loading tags...`, },
done: !!tags, {
}, label: _(msg`Loading tags...`),
] done: tagsLoaded,
},
const loading = steps.some(s => !s.done) ]
const loadingPercentage = Math.round((100.0 * steps.filter(s => s.done).length) / steps.length)
const loadingStepLabel = steps.find(s => !s.done)?.label const loading = steps.some(s => !s.done)
const loadingPercentage = Math.round((100.0 * steps.filter(s => s.done).length) / steps.length)
return { steps, loading, loadingPercentage, loadingStepLabel } const loadingStepLabel = steps.find(s => !s.done)?.label
}
return { steps, loading, loadingPercentage, loadingStepLabel }
}

View File

@@ -1,64 +1,64 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
export const useBrowserExtension = () => { export const useBrowserExtension = () => {
// the extension will set the "browser-extension-installed" attribute on the root element // the extension will set the "browser-extension-installed" attribute on the root element
const [browserExtensionVersion, setBrowserExtensionVersion] = useState( const [browserExtensionVersion, setBrowserExtensionVersion] = useState(
document.documentElement.getAttribute("browser-extension-installed") document.documentElement.getAttribute("browser-extension-installed")
) )
// monitor the attribute on the root element as it may change after the page was loaded // monitor the attribute on the root element as it may change after the page was loaded
useEffect(() => { useEffect(() => {
const observer = new MutationObserver(mutations => { const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => { for (const mutation of mutations) {
if (mutation.type === "attributes") { if (mutation.type === "attributes") {
const element = mutation.target as Element const element = mutation.target as Element
const version = element.getAttribute("browser-extension-installed") const version = element.getAttribute("browser-extension-installed")
if (version) setBrowserExtensionVersion(version) if (version) setBrowserExtensionVersion(version)
} }
}) }
}) })
observer.observe(document.documentElement, { observer.observe(document.documentElement, {
attributes: true, attributes: true,
}) })
return () => observer.disconnect() return () => observer.disconnect()
}, []) }, [])
// when not in an iframe, window.parent is a reference to window // when not in an iframe, window.parent is a reference to window
const isBrowserExtensionPopup = window.parent !== window const isBrowserExtensionPopup = window.parent !== window
const isBrowserExtensionInstalled = isBrowserExtensionPopup || !!browserExtensionVersion const isBrowserExtensionInstalled = isBrowserExtensionPopup || !!browserExtensionVersion
const isBrowserExtensionInstallable = !isBrowserExtensionPopup const isBrowserExtensionInstallable = !isBrowserExtensionPopup
const w = isBrowserExtensionPopup ? window.parent : window const w = isBrowserExtensionPopup ? window.parent : window
const openSettingsPage = () => w.postMessage("open-settings-page", "*") const openSettingsPage = () => w.postMessage("open-settings-page", "*")
const openAppInNewTab = () => w.postMessage("open-app-in-new-tab", "*") const openAppInNewTab = () => w.postMessage("open-app-in-new-tab", "*")
const openLinkInBackgroundTab = (url: string) => { const openLinkInBackgroundTab = (url: string) => {
if (isBrowserExtensionInstalled) { if (isBrowserExtensionInstalled) {
w.postMessage(`open-link-in-background-tab:${url}`, "*") w.postMessage(`open-link-in-background-tab:${url}`, "*")
} else { } else {
// fallback to ctrl+click simulation // fallback to ctrl+click simulation
const a = document.createElement("a") const a = document.createElement("a")
a.href = url a.href = url
a.rel = "noreferrer" a.rel = "noreferrer"
a.dispatchEvent( a.dispatchEvent(
new MouseEvent("click", { new MouseEvent("click", {
ctrlKey: true, ctrlKey: true,
metaKey: true, metaKey: true,
}) })
) )
} }
} }
const setBadgeUnreadCount = (count: number | string) => w.postMessage(`set-badge-unread-count:${count}`, "*") const setBadgeUnreadCount = (count: number | string) => w.postMessage(`set-badge-unread-count:${count}`, "*")
return { return {
browserExtensionVersion, browserExtensionVersion,
isBrowserExtensionInstallable, isBrowserExtensionInstallable,
isBrowserExtensionInstalled, isBrowserExtensionInstalled,
isBrowserExtensionPopup, isBrowserExtensionPopup,
openSettingsPage, openSettingsPage,
openAppInNewTab, openAppInNewTab,
openLinkInBackgroundTab, openLinkInBackgroundTab,
setBadgeUnreadCount, setBadgeUnreadCount,
} }
} }

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