Compare commits

...

76 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
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
63 changed files with 1009 additions and 507 deletions

View File

@@ -44,18 +44,27 @@ jobs:
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
# Build pages
- name: Copy generated markdown documentation to /documentation
run: mkdir documentation && cp ./commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md ./documentation/README.md
- name: Create pages directory structure
run: mkdir -p target/pages/documentation/custom-css
- name: Generate pages
uses: wranders/markdown-to-pages-action@8d8a750832932ac785f5424c8c5543aa0b26bb9a # v1
- name: Convert readme file to html
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
with:
token: ${{ secrets.GITHUB_TOKEN }}
out_path: target/pages
files: |-
README.md
documentation/README.md
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
@@ -134,7 +143,7 @@ jobs:
## build but don't push for PRs and renovate
- name: Docker build - native
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
@@ -142,7 +151,7 @@ jobs:
platforms: linux/amd64,linux/arm64/v8
- name: Docker build - jvm
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
@@ -151,7 +160,7 @@ jobs:
## build and push tag
- name: Docker build and push tag - native
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -163,7 +172,7 @@ jobs:
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
- name: Docker build and push tag - jvm
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -176,7 +185,7 @@ jobs:
## build and push master
- name: Docker build and push master - native
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_name == 'master' }}
with:
context: .
@@ -186,7 +195,7 @@ jobs:
tags: athou/commafeed:master-${{ matrix.database }}
- name: Docker build and push master - jvm
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: ${{ github.ref_name == 'master' }}
with:
context: .

View File

@@ -1,5 +1,18 @@
# Changelog
## [5.10.0]
- Add an indicator next to each feed's unread count in the tree to show when new entries are discovered while the app is open (#1762)
- Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again
- The aarch64 native executable now also works on the Raspberry Pi 5 (#1795)
- Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are displayed (#1087)
## [5.9.0]
- A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757)
- Added a link in the README to the [documentation](https://athou.github.io/commafeed/documentation/custom-css/) of the new CSS classes
- Static resources are now cached for much longer (#1782)
## [5.8.0]
- A color picker is now available on the settings page to change the orange accent of the application (#1598)

View File

@@ -17,6 +17,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc
- REST API
- 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)
- Compiles to native code for blazing fast startup and low memory usage
- Supports 4 databases

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,14 @@
"@fontsource/open-sans": "^5.2.5",
"@lingui/core": "^5.3.1",
"@lingui/react": "^5.3.1",
"@mantine/core": "^8.0.0",
"@mantine/form": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"@mantine/modals": "^8.0.0",
"@mantine/notifications": "^8.0.0",
"@mantine/spotlight": "^8.0.0",
"@mantine/core": "^8.0.2",
"@mantine/form": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@mantine/modals": "^8.0.2",
"@mantine/notifications": "^8.0.2",
"@mantine/spotlight": "^8.0.2",
"@monaco-editor/react": "^4.7.0",
"@reduxjs/toolkit": "^2.7.0",
"@reduxjs/toolkit": "^2.8.2",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"escape-string-regexp": "^5.0.0",
@@ -43,13 +43,13 @@
"react-icons": "^5.5.0",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^9.2.0",
"react-router-dom": "^7.5.3",
"react-router-dom": "^7.6.1",
"react-swipeable": "^7.0.2",
"redoc": "^2.5.0",
"style-to-object": "^1.0.8",
"throttle-debounce": "^5.0.2",
"tinycon": "^0.6.8",
"tss-react": "^4.9.17",
"tss-react": "^4.9.18",
"websocket-heartbeat-js": "^1.1.3"
},
"devDependencies": {
@@ -61,20 +61,21 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/mousetrap": "^1.6.15",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"@types/react-infinite-scroller": "^1.2.5",
"@types/throttle-debounce": "^5.0.2",
"@types/tinycon": "^0.6.7",
"@vitejs/plugin-react": "^4.4.1",
"@vitejs/plugin-react": "^4.5.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-react-compiler": "^19.1.0-rc.2",
"jsdom": "^26.1.0",
"rollup-plugin-visualizer": "^5.14.0",
"rollup-plugin-visualizer": "^6.0.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-checker": "^0.9.2",
"vite-plugin-checker": "^0.9.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3"
"vitest": "^3.1.4"
},
"overrides": {
"react-infinite-scroller": {

View File

@@ -6,16 +6,16 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.8.0</version>
<version>5.10.0</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
<properties>
<!-- renovate: datasource=node-version depName=node -->
<node.version>v22.15.0</node.version>
<node.version>v22.16.0</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>11.3.0</npm.version>
<npm.version>11.4.1</npm.version>
</properties>
<build>

View File

@@ -143,11 +143,15 @@ function GoogleAnalyticsHandler() {
return null
}
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
function UnreadCountTitleHandler({ enabled }: { enabled?: boolean }) {
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCount = categoryUnreadCount(root)
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title>
}
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
function UnreadCountFaviconHandler({ enabled }: { enabled?: boolean }) {
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCount = categoryUnreadCount(root)
useEffect(() => {
if (enabled && unreadCount > 0) {
Tinycon.setBubble(unreadCount)
@@ -205,13 +209,10 @@ function CustomCssHandler() {
export function App() {
useI18n()
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const dispatch = useAppDispatch()
const unreadCount = categoryUnreadCount(root)
useEffect(() => {
dispatch(reloadServerInfos())
}, [dispatch])
@@ -219,8 +220,8 @@ export function App() {
return (
<Providers>
<>
<UnreadCountTitleHandler unreadCount={unreadCount} enabled={unreadCountTitle} />
<UnreadCountFaviconHandler unreadCount={unreadCount} enabled={unreadCountFavicon} />
<UnreadCountTitleHandler enabled={unreadCountTitle} />
<UnreadCountFaviconHandler enabled={unreadCountFavicon} />
<BrowserExtensionBadgeUnreadCountHandler />
<CustomJsHandler />
<CustomCssHandler />

View File

@@ -108,5 +108,6 @@ export const Constants = {
delay: 500,
},
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
}

View File

@@ -1,12 +1,22 @@
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 { collapseTreeCategory, reloadTree } from "app/tree/thunks"
import type { Category } from "app/types"
import { visitCategoryTree } from "app/utils"
import type { Category, Subscription } from "app/types"
import { flattenCategoryTree, visitCategoryTree } from "app/utils"
export interface TreeSubscription extends Subscription {
// client-side only flag
hasNewEntries?: boolean
}
export interface TreeCategory extends Category {
feeds: TreeSubscription[]
children: TreeCategory[]
}
interface TreeState {
rootCategory?: Category
rootCategory?: TreeCategory
mobileMenuOpen: boolean
sidebarVisible: boolean
}
@@ -37,12 +47,27 @@ export const treeSlice = createSlice({
visitCategoryTree(state.rootCategory, c => {
for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) {
f.unread += action.payload.amount
f.hasNewEntries = true
}
})
},
},
extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => {
// set hasNewEntries to true if new unread > previous unread
if (state.rootCategory) {
const oldFeeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
const oldFeedsById = new Map(oldFeeds.map(f => [f.id, f]))
const newFeeds = flattenCategoryTree(action.payload).flatMap(c => c.feeds)
for (const newFeed of newFeeds) {
const oldFeed = oldFeedsById.get(newFeed.id)
if (oldFeed && newFeed.unread > oldFeed.unread) {
newFeed.hasNewEntries = true
}
}
}
state.rootCategory = action.payload
})
builder.addCase(collapseTreeCategory.pending, (state, action) => {
@@ -59,6 +84,25 @@ export const treeSlice = createSlice({
}
})
})
builder.addCase(loadEntries.fulfilled, (state, action) => {
if (!state.rootCategory) return
const { source } = action.meta.arg
if (source.type === "category") {
visitCategoryTree(state.rootCategory, c => {
if (c.id === source.id) {
for (const f of flattenCategoryTree(c).flatMap(c => c.feeds)) {
f.hasNewEntries = false
}
}
})
} else if (source.type === "feed") {
const feeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
for (const f of feeds.filter(f => f.id === +source.id)) {
f.hasNewEntries = false
}
}
})
builder.addCase(redirectTo, state => {
state.mobileMenuOpen = false
})

View File

@@ -1,8 +1,13 @@
import { configureStore } from "@reduxjs/toolkit"
import { client } from "app/client"
import { loadEntries } from "app/entries/thunks"
import { type RootState, reducers } from "app/store"
import { selectNextUnreadTreeItem } from "app/tree/thunks"
import type { Category, Subscription } from "app/types"
import { describe, expect, it } from "vitest"
import { 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,
@@ -117,3 +122,51 @@ describe("selectNextUnreadTreeItem", () => {
expect(store.getState().redirect.to).toBe("/app/feed/3")
})
})
describe("hasNewEntries", () => {
beforeEach(() => {
vi.resetAllMocks()
})
it("sets and clear flag for a feed", async () => {
vi.mocked(client.feed.getEntries).mockResolvedValue({
data: {
entries: [{ id: "3" } as Entry],
hasMore: false,
name: "my-feed",
errorCount: 3,
feedLink: "https://mysite.com/feed",
timestamp: 123,
ignoredReadStatus: false,
},
} as AxiosResponse<Entries>)
const store = configureStore({
reducer: reducers,
preloadedState: {
tree: {
rootCategory: root,
},
entries: {
source: {
type: "feed",
id: "1",
},
},
} as RootState,
})
// initial state
expect(store.getState().tree.rootCategory?.children[0].feeds[0].unread).toBe(0)
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBeFalsy()
// increments unread count and sets hasNewEntries to true
await store.dispatch(newFeedEntriesDiscovered({ feedId: 1, amount: 3 }))
expect(store.getState().tree.rootCategory?.children[0].feeds[0].unread).toBe(3)
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBe(true)
// reload entries and sets hasNewEntries to false
await store.dispatch(loadEntries({ source: { type: "feed", id: "1" }, clearSearch: true }))
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBe(false)
})
})

View File

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

View File

@@ -29,7 +29,7 @@ export const ActionButton = forwardRef<HTMLDivElement, ActionButtonProps>((props
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
return (
<Box ref={ref}>
<Box ref={ref} className="cf-action-button">
{iconOnly && (
<Tooltip label={label} openDelay={Constants.tooltip.delay}>
<ActionIcon

View File

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

View File

@@ -288,7 +288,7 @@ export function FeedEntries() {
return (
<InfiniteScroll
id="entries"
className={`view-mode-${viewMode}`}
className={`cf-entries cf-view-mode-${viewMode}`}
initialLoad={false}
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
hasMore={hasMore}

View File

@@ -184,10 +184,10 @@ export function FeedEntry(props: FeedEntryProps) {
</a>
{props.expanded && (
<Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
<Box className={classes.body}>
<Box className={`${classes.body} cf-content`}>
<FeedEntryBody entry={props.entry} />
</Box>
<Divider variant="dashed" my={paddingY} />
<Divider variant="dashed" my={paddingY} className="cf-footer-divider" />
<FeedEntryFooter entry={props.entry} />
</Box>
)}

View File

@@ -37,7 +37,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
)
return (
<Group justify="space-between">
<Group justify="space-between" className="cf-footer">
<Group gap={spacing}>
{props.entry.markable && (
<ActionButton

View File

@@ -29,8 +29,8 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
read: props.entry.read,
})
return (
<Box>
<Flex align="flex-start" justify="space-between">
<Box className="cf-header">
<Flex align="flex-start" justify="space-between" className="cf-header-title">
<Flex align="flex-start" className={classes.main}>
{props.showStarIcon && (
<Box ml={-5}>
@@ -41,7 +41,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
</Flex>
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
</Flex>
<Flex align="center">
<Flex align="center" className="cf-header-subtitle">
<FeedFavicon url={props.entry.iconUrl} />
<Space w={6} />
<Box c="dimmed">
@@ -51,7 +51,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
</Box>
</Flex>
{props.expanded && (
<Box c="dimmed">
<Box className="cf-header-details">
{props.entry.author && <span>by {props.entry.author}</span>}
{props.entry.author && props.entry.categories && <span>&nbsp;·&nbsp;</span>}
{props.entry.categories && <span>{props.entry.categories}</span>}

View File

@@ -42,11 +42,14 @@ function HeaderToolbar(props: { children: React.ReactNode }) {
display: "flex",
justifyContent: "space-between",
}}
className="cf-toolbar"
>
{props.children}
</Box>
) : (
<Group gap={spacing}>{props.children}</Group>
<Group gap={spacing} className="cf-toolbar">
{props.children}
</Group>
)
}
@@ -75,7 +78,7 @@ export function Header() {
if (!settings) return <Loader />
return (
<Center>
<Center className="cf-toolbar-wrapper">
<HeaderToolbar>
<ActionButton
icon={<TbArrowUp size={iconSize} />}

View File

@@ -1,7 +1,8 @@
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 { client, errorToStrings } from "app/client"
import { Constants } from "app/constants"
import { redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert"
@@ -57,13 +58,27 @@ export function CustomCodeSettings() {
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Stack>
<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"
{...form.getInputProps("customCss")}
/>
<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"
{...form.getInputProps("customJs")}
/>

View File

@@ -10,9 +10,10 @@ import {
redirectToTagDetails,
} from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import type { TreeSubscription } from "app/tree/slice"
import { collapseTreeCategory } from "app/tree/thunks"
import type { Category, Subscription } from "app/types"
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "app/utils"
import { Loader } from "components/Loader"
import { OnDesktop } from "components/responsive/OnDesktop"
import React from "react"
@@ -89,6 +90,7 @@ export function Tree() {
name={<Trans>All</Trans>}
icon={allIcon}
unread={categoryUnreadCount(root)}
hasNewEntries={categoryHasNewEntries(root)}
selected={source.type === "category" && source.id === Constants.categories.all.id}
expanded={false}
level={0}
@@ -103,6 +105,7 @@ export function Tree() {
name={<Trans>Starred</Trans>}
icon={starredIcon}
unread={0}
hasNewEntries={false}
selected={source.type === "category" && source.id === Constants.categories.starred.id}
expanded={false}
level={0}
@@ -122,6 +125,7 @@ export function Tree() {
name={category.name}
icon={category.expanded ? expandedIcon : collapsedIcon}
unread={categoryUnreadCount(category)}
hasNewEntries={categoryHasNewEntries(category)}
selected={source.type === "category" && source.id === category.id}
expanded={category.expanded}
level={level}
@@ -133,7 +137,7 @@ export function Tree() {
)
}
const feedNode = (feed: Subscription, level = 0) => {
const feedNode = (feed: TreeSubscription, level = 0) => {
if (!isFeedDisplayed(feed)) return null
return (
@@ -143,6 +147,7 @@ export function Tree() {
name={feed.name}
icon={feed.iconUrl}
unread={feed.unread}
hasNewEntries={!!feed.hasNewEntries}
selected={source.type === "feed" && source.id === String(feed.id)}
level={level}
hasError={feed.errorCount > errorThreshold}
@@ -159,6 +164,7 @@ export function Tree() {
name={tag}
icon={tagIcon}
unread={0}
hasNewEntries={false}
selected={source.type === "tag" && source.id === tag}
level={0}
hasError={false}
@@ -182,7 +188,7 @@ export function Tree() {
<OnDesktop>
<TreeSearch feeds={feeds} />
</OnDesktop>
<Box>
<Box className="cf-tree">
{allCategoryNode()}
{starredCategoryNode()}
{root.children.map(c => recursiveCategoryNode(c))}

View File

@@ -15,6 +15,7 @@ interface TreeNodeProps {
expanded?: boolean
level: number
hasError: boolean
hasNewEntries: boolean
onClick: (e: React.MouseEvent, id: string) => void
onIconClick?: (e: React.MouseEvent, id: string) => void
}
@@ -68,19 +69,19 @@ export function TreeNode(props: TreeNodeProps) {
<Box
py={1}
pl={props.level * 20}
className={classes.node}
className={`${classes.node} cf-treenode cf-treenode-${props.type}`}
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
data-id={props.id}
data-type={props.type}
data-unread-count={props.unread}
>
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)} 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>
<UnreadCount unreadCount={props.unread} />
<Box className="cf-treenode-unread-count">
<UnreadCount unreadCount={props.unread} showIndicator={props.hasNewEntries} />
</Box>
)}
</Box>

View File

@@ -1,7 +1,7 @@
import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro"
import { TextInput } from "@mantine/core"
import { Box, TextInput } from "@mantine/core"
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
import { redirectToFeed } from "app/redirect/thunks"
import { useAppDispatch } from "app/store"
@@ -33,7 +33,7 @@ export function TreeSearch(props: TreeSearchProps) {
useMousetrap("g u", () => spotlight.open())
return (
<>
<Box className="cf-treesearch">
<TextInput
placeholder={_(msg`Search`)}
leftSection={searchIcon}
@@ -58,6 +58,6 @@ export function TreeSearch(props: TreeSearchProps) {
}}
nothingFound={<Trans>Nothing found</Trans>}
/>
</>
</Box>
)
}

View File

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

View File

@@ -8,28 +8,28 @@ interface Step {
}
export const useAppLoading = () => {
const profile = useAppSelector(state => state.user.profile)
const settings = useAppSelector(state => state.user.settings)
const rootCategory = useAppSelector(state => state.tree.rootCategory)
const tags = useAppSelector(state => state.user.tags)
const profileLoaded = useAppSelector(state => !!state.user.profile)
const settingsLoaded = useAppSelector(state => !!state.user.settings)
const rootCategoryLoaded = useAppSelector(state => !!state.tree.rootCategory)
const tagsLoaded = useAppSelector(state => !!state.user.tags)
const { _ } = useLingui()
const steps: Step[] = [
{
label: _(msg`Loading settings...`),
done: !!settings,
done: settingsLoaded,
},
{
label: _(msg`Loading profile...`),
done: !!profile,
done: profileLoaded,
},
{
label: _(msg`Loading subscriptions...`),
done: !!rootCategory,
done: rootCategoryLoaded,
},
{
label: _(msg`Loading tags...`),
done: !!tags,
done: tagsLoaded,
},
]

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0> هل تحتاج إلى حساب؟ </0> <1> اشترك! </ 1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>Ei,</0><1> sóc la Jérémie de Bèlgica i fa més de 10 anys que tre
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Necessites un compte?</0><1>Registreu-vos!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Potřebujete účet?</0><1>Zaregistrujte se!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Angen cyfrif?</0><1>Ymunwch!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Har du brug for en konto?</0><1>Tilmeld dig!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>Hey,</0><1>Ich bin Jérémie aus Belgien und arbeite seit über 10 Ja
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaF
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Need an account?</0><1>Sign up!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr "<0>See </0><1>here</1><2> for more information.</2>"
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Hola,</0><1>Soy Jérémie de Bélgica y he estado trabajando en Comma
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>¿Necesitas una cuenta?</0><1>¡Regístrate!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>به یک حساب نیاز دارید؟</0><1>ثبت نام کنید!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Tarvitsetko tilin?</0><1>Rekisteröidy!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>Salut,</0><1>Je m'appelle Jérémie, je suis belge, et je développe
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Besoin d'un compte ?</0><1>Enregistrez-vous !</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Necesitas unha conta?</0><1>Rexístrate!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Fiókra van szüksége?</0><1>Regisztráljon!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Butuh akun?</0><1>Daftar!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Hai bisogno di un account?</0><1>Registrati!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>こんにちは、</0><1>私はベルギーのジェレミーです
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>アカウントが必要ですか?</0><1>サインアップ!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>계정이 필요하십니까?</0><1>가입하세요!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Perlukan akaun?</0><1>Daftar!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Een account nodig?</0><1>Meld je aan!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Potrzebujesz konta?</0><1>Zarejestruj się!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Precisa de uma conta?</0><1>Inscreva-se!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>Здравствуйте,</0><1>Я Жереми из Бельгии,
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Нужен аккаунт?</0><1>Зарегистрируйтесь!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Potrebujete účet?</0><1>Zaregistrujte sa!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Behöver du ett konto?</0><1>Registrera dig!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>Merhaba,</0><1>Ben Belçika'dan Jérémie ve 10 yıldır boş zamanla
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Bir hesaba mı ihtiyacınız var?</0><1>Kaydolun!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"

View File

@@ -33,6 +33,10 @@ msgstr "<0>您好,</0><1>我是来自比利时的Jérémie已经在业余时
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>需要一个帐户?</0><1>注册!</1>"
#: src/components/settings/CustomCodeSettings.tsx
msgid "<0>See </0><1>here</1><2> for more information.</2>"
msgstr ""
#: src/pages/app/AboutPage.tsx
#: src/components/header/ProfileMenu.tsx
msgid "About"
@@ -389,7 +393,7 @@ msgstr "过滤表达式"
#: src/components/header/ProfileMenu.tsx
msgid "Font size"
msgstr ""
msgstr "字号"
#: src/components/header/ProfileMenu.tsx
msgid "Force fetching feeds is not yet available."

View File

@@ -42,7 +42,9 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
const { id = Constants.categories.all.id } = useParams()
const viewport = useViewportSize()
const theme = useMantineTheme()
const rootCategory = useAppSelector(state => state.tree.rootCategory)
const noSubscriptions = useAppSelector(
state => state.tree.rootCategory && flattenCategoryTree(state.tree.rootCategory).every(c => c.feeds.length === 0)
)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
const hasMore = useAppSelector(state => state.entries.hasMore)
@@ -83,12 +85,11 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
return () => promise.abort()
}, [dispatch, props.sourceType, id, location.state?.timestamp])
const noSubscriptions = rootCategory && flattenCategoryTree(rootCategory).every(c => c.feeds.length === 0)
if (noSubscriptions) return <NoSubscriptionHelp />
return (
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
<Box mb={viewport.height * 0.7}>
<Group gap="xl">
<Group gap="xl" className="cf-entries-title">
{sourceWebsiteUrl && (
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
<Title order={3}>{title}</Title>

View File

@@ -35,9 +35,16 @@ interface LayoutProps {
function LogoAndTitle() {
const dispatch = useAppDispatch()
return (
<Center inline onClick={async () => await dispatch(redirectToRootCategory())} style={{ cursor: "pointer" }}>
<Logo size={24} />
<Title order={3} pl="md">
<Center
className="cf-logo-title"
inline
onClick={async () => await dispatch(redirectToRootCategory())}
style={{ cursor: "pointer" }}
>
<Box className="cf-logo">
<Logo size={24} />
</Box>
<Title order={3} pl="md" className="cf-title">
CommaFeed
</Title>
</Center>

View File

@@ -10,7 +10,7 @@ export default defineConfig(() => ({
plugins: [
react({
babel: {
plugins: ["@lingui/babel-plugin-lingui-macro"],
plugins: [["babel-plugin-react-compiler", { target: "19" }], "@lingui/babel-plugin-lingui-macro"],
},
}),
lingui(),

View File

@@ -6,16 +6,16 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.8.0</version>
<version>5.10.0</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
<properties>
<quarkus.version>3.22.1</quarkus.version>
<quarkus.version>3.22.3</quarkus.version>
<querydsl.version>6.11</querydsl.version>
<rome.version>2.1.0</rome.version>
<swagger.version>2.2.30</swagger.version>
<swagger.version>2.2.32</swagger.version>
<build.database>h2</build.database>
</properties>
@@ -221,7 +221,7 @@
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<version>9.0.1</version>
<version>9.0.2</version>
<executions>
<execution>
<goals>
@@ -269,7 +269,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.23.1</version>
<version>10.24.0</version>
</dependency>
</dependencies>
<executions>
@@ -327,7 +327,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>5.8.0</version>
<version>5.10.0</version>
</dependency>
<!-- compile-time processors -->
@@ -391,7 +391,7 @@
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId>
<version>4.2.30</version>
<version>4.2.32</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
@@ -483,7 +483,7 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.4.4</version>
<version>5.5</version>
</dependency>
<!-- add brotli support for httpclient5 -->
<dependency>

View File

@@ -1,4 +1,4 @@
FROM ibm-semeru-runtimes:open-21.0.6_7-jre@sha256:bb59a7eb5bfab35cb112c403521312079a8fcfb85e9e3ca9afe8fc24ee57ae94
FROM ibm-semeru-runtimes:open-21.0.7_6-jre@sha256:81475a86a6abb3b62feae30112f13f7da20b8c6968eba0d3e7c76016200e6b56
EXPOSE 8082
RUN mkdir -p /commafeed/data

View File

@@ -1,4 +1,4 @@
FROM debian:12.10@sha256:264982ff4d18000fa74540837e2c43ca5137a53a83f8f62c7b3803c0f0bdcd56
FROM debian:12.11@sha256:bd73076dc2cd9c88f48b5b358328f24f2a4289811bd73787c031e20db9f97123
ARG TARGETARCH
EXPOSE 8082

View File

@@ -151,7 +151,7 @@ public class HttpGetter {
}
private void ensureHttpScheme(String scheme) throws SchemeNotAllowedException {
if (!"http".equals(scheme) && !"https".equals(scheme)) {
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
throw new SchemeNotAllowedException(scheme);
}
}

View File

@@ -4,6 +4,7 @@ quarkus.http.test-port=8085
quarkus.http.enable-compression=true
# http cache
quarkus.http.static-resources.max-age=P365d
## make sure the webapp is always up to date
quarkus.http.filter.index-html.header."Cache-Control"=no-cache
quarkus.http.filter.index-html.matches=/
@@ -37,6 +38,8 @@ quarkus.shutdown.timeout=5s
# native
quarkus.native.march=compatibility
quarkus.native.add-all-charsets=true
# fix for https://github.com/Athou/commafeed/issues/1795
quarkus.native.additional-build-args=-H:PageSize=65536
# dev profile overrides

View File

@@ -18,6 +18,6 @@ class StaticFilesIT {
@ParameterizedTest
@ValueSource(strings = { "/favicon.ico" })
void servedWithCache(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=86400");
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=31536000");
}
}

121
documentation/CUSTOMCSS.md Normal file
View File

@@ -0,0 +1,121 @@
# Custom CSS Guide
On the Admin settings page, there is a tab for "Custom Code" where you can enter [CSS](https://en.wikipedia.org/wiki/CSS) to customize the look & feel of CommaFeed. Many of the HTML elements of CommaFeed have been given specific class names to make it easier to write CSS rules. For example, the header of a feed entry is enclosed in an HTML `<div>` element with the class name `cf-header`. So if you wanted to modify the appearance of all feed entry headers, you could use that specific class name to select all feed entry headers and apply whatever changes you desired. For example, if you want to make change the background color of the headers, you could add this to the custom CSS code:
```
.cf-header {background-color: lightblue;}
```
The tables below list useful page elements as well as all of the CommaFeed specific class names, and describes their corresponding page elements. These elements are selected to provide a good "starting point" within each portion of the feed reading page. Sometimes that's all you'll need, but to modify some elements of the page, you will have to start at one of the CommaFeed specific class names and then "drill down" to the HTML element you wish to change. A useful approach is to use your web browser's Inspector to find the element you want to change. (Typically you can hit F12 to bring up the web developer tools.) Find the element you want to change. If it has a "cf-" class name from the table below, then you can modify it directly using the formula illustrated above with "cf-header". If it doesn't have a "cf-" classname, then search upwards through the enclosing elements until you find one that does. Use that name as your starting point and extend your CSS selector down to the element you want to modify.
If you're having trouble writing a CSS rule, changing the background color as in the rule above can be helpful to see what elements your selector is selecting -- or if it is not selecting anything!
## Extended Example
The extended example below modifies the CommaFeed interface to be more minimal and illustrates a variety of uses of the CommaFeed class names and CSS. There are examples of styling CommaFeed classes directly as well as examples of more complicated selection rules.
```
/* GENERAL (changes applied to everything) */
main {font-size: 14px; font-family: sans-serif; line-height: 1.35; padding-top: calc(1rem * 2.5) !important;}
/* Don't force font-size on blockquotes and make them italic */
blockquote {font-size: unset !important; font-style: italic;}
/* Make all the button icons black */
header svg {stroke: black !important; }
main > svg {stroke: black !important; }
/* Make links in articles light blue with a hover underline */
article a:not([class]) { color:#428bca; text-decoration:none; }
article a:not([class]):hover { text-decoration:underline; }
/* Make HTML headers the (same) reasonable size */
h3 {font-size: 16px !important;}
h2 {font-size: 16px !important;}
h1 {font-size: 16px !important;}
/* Make buttons actual size */
main > button {min-width: unset !important; min-height: unset !important;}
/* HEADER (tool bar at the top of the page) */
/* Make the header more compact */
header > div > div {padding-bottom: 0 !important; padding-top: 0 !important;}
/* Let the toolbar pull to the left */
.cf-toolbar-wrapper {justify-content: unset !important;}
/* Minimize height of the toolbar */
header {height: unset !important;}
/* Move buttons closer together */
header img {width: calc(1rem) !important;}
/* No button labels, even if there's room. */
.cf-toolbar-wrapper .mantine-Button-label {display: none;}
/* SIDEBAR (where the feeds are listed) */
/* Specific font and layout changes for the entire sidebar */
.cf-tree {font-size: 14px; font-weight: 700; font-family: sans-serif; line-height: 150%; top: 30px !important;}
.cf-treenode {margin-right: 0}
/* Make unread category names black */
.cf-treenode-category {color: black !important;}
/* Remove the favicons for the feeds in the sidebar */
.cf-treenode-icon {display: none;}
/* Make the unread counts lighter, gray and in parens */
.cf-badge {display: flex; font-weight: 300; color: gray; background-color: unset; align-items: unset;}
.cf-badge::before {content: "(";}
.cf-badge::after {content: ")";}
/* FEED ENTRIES */
/* Only changes Detailed and Expanded display */
/* Remove subtitle and details in feed entries, just leaving the title */
.cf-header-subtitle {display: none;}
.cf-header-details {display: none;}
/* Remove the divider and button bar at the bottom of feed entries */
.cf-footer-divider {display: none;}
.cf-footer {display: none;}
/* MISCELLANEOUS */
/* An example of changing the content: Add an extra space before the submitted line on Reddit feed entries. */
article span > div::after {content: "\A"; white-space: pre;}
```
## CommaFeed Useful Elements
The table below shows some elements of the CommaFeed main page that are useful for applying custom CSS. Note that these are elements, not class names, so you must use these without the leading period used to reference a class name. For example:
```
article {background-color: lightblue;}
```
|Element Name|Element Description|
|---|---|
|main|The entire web page|
|header|The header area (logo and toolbar)|
|nav|The entire sidebar|
|footer|The footer area at the bottom of the page|
|article|Entire feed entry|
|h3, h2, h1|HTML headers|
## CommaFeed Class Names
The table below shows the CommaFeed specific class names. To reference a class name in a CSS rule, use a leading period. For example:
```
.cf-header {background-color: lightblue;}
```
|Class Name|Element Description|
|---|---|
|cf-logo-title|The CommaFeed logo and title in upper left of page|
|cf-logo|The CommaFeed logo|
|cf-title|The CommaFeed title|
|cf-toolbar|The entire toolbar of action buttons at the top of the page|
|cf-action-button|Each button within the toolbar. (Note: also used in feed entry footer.)|
|cf-treesearch|The search box at the top of the sidebar|
|cf-tree|The entire feed tree in the sidebar|
|cf-treenode|All nodes in the feed tree|
|cf-treenode-category|Category nodes in the feed tree|
|cf-treenode-feed|Feed nodes in the feed tree|
|cf-treenode-icon|Icon within feed nodes|
|cf-treenode-unread-count|Unread count within feed nodes|
|cf-badge|The badge for the unread count|
|cf-entries-title|Title of feed currently displayed in the content area|
|cf-entries|All of the feed entries being displayed in the content area|
|cf-header|The header of a feed entry|
|cf-header-title|The first line in the header of a feed entry (the entry title)|
|cf-header-subtitle|The second line in the header of a feed entry (feed name and time of entry)|
|cf-header-details|The third line in the header of a feed entry (typically author, subject, etc.)|
|cf-content|The content (body) of a feed entry|
|cf-footer-divider|The divider between the feed entry content and the feed entry footer|
|cf-footer|The feed entry footer (buttons to share, star, etc.)|
|cf-action-button|Each button within the feed entry footer. (note: also used in toolbar.)|

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.8.0</version>
<version>5.10.0</version>
<name>CommaFeed</name>
<packaging>pom</packaging>