Compare commits

...

69 Commits
5.4.0 ... 5.5.0

Author SHA1 Message Date
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
34 changed files with 711 additions and 609 deletions

View File

@@ -1,6 +1,10 @@
name: ci
permissions:
contents: read
on: [ push ]
on:
push:
pull_request:
env:
JAVA_VERSION: 21
@@ -8,6 +12,8 @@ env:
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-24.04-arm", "windows-latest" ]
@@ -20,13 +26,13 @@ jobs:
run: git config --global core.autocrlf false
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
# Setup
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm"
@@ -42,21 +48,22 @@ jobs:
# Upload artifacts
- name: Upload cross-platform app
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # 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
overwrite: true
- name: Upload native executable
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
path: commafeed-server/target/commafeed-*-runner*
docker:
needs: build
runs-on: ubuntu-latest
needs: build
if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }}
strategy:
matrix:
@@ -65,23 +72,23 @@ jobs:
steps:
# Checkout
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
- name: Install required packages
run: sudo apt-get install -y rename unzip
# Prepare artifacts
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
pattern: commafeed-${{ matrix.database }}-*
path: ./artifacts
@@ -102,15 +109,14 @@ jobs:
# Docker
- name: Login to Container Registry
uses: docker/login-action@v3
if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }}
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
## tags
- name: Docker build and push tag - native
uses: docker/build-push-action@v6
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -122,7 +128,7 @@ jobs:
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
- name: Docker build and push tag - jvm
uses: docker/build-push-action@v6
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -135,7 +141,7 @@ jobs:
## master
- name: Docker build and push master - native
uses: docker/build-push-action@v6
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
if: ${{ github.ref_name == 'master' }}
with:
context: .
@@ -145,7 +151,7 @@ jobs:
tags: athou/commafeed:master-${{ matrix.database }}
- name: Docker build and push master - jvm
uses: docker/build-push-action@v6
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
if: ${{ github.ref_name == 'master' }}
with:
context: .
@@ -158,16 +164,19 @@ jobs:
runs-on: ubuntu-latest
needs:
- build
- docker
permissions:
contents: write
if: github.ref_type == 'tag'
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
pattern: commafeed-*
path: ./artifacts
@@ -177,20 +186,20 @@ jobs:
run: chmod +x artifacts/*-runner
- name: Extract Changelog Entry
uses: mindsers/changelog-reader-action@v2
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2
id: changelog_reader
with:
version: ${{ github.ref_name }}
- name: Create GitHub release
uses: ncipollo/release-action@v1
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1
with:
name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }}
artifacts: ./artifacts/*
- name: Update Docker Hub Description
uses: peter-evans/dockerhub-description@v4
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -1,5 +1,11 @@
# Changelog
## [5.5.0]
- 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
- Fix an issue with some labels not correctly internationalized
## [5.4.0]
- An arm64 native executable is now available for download on the releases page

View File

@@ -94,7 +94,7 @@ 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 prefixed with `-D` (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)

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -17,64 +17,69 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@fontsource/open-sans": "^5.1.1",
"@mantine/core": "^7.16.0",
"@mantine/form": "^7.16.0",
"@mantine/hooks": "^7.16.0",
"@mantine/modals": "^7.16.0",
"@mantine/notifications": "^7.16.0",
"@mantine/spotlight": "^7.16.0",
"@lingui/core": "^5.1.2",
"@lingui/react": "^5.1.2",
"@monaco-editor/react": "^4.6.0",
"@reduxjs/toolkit": "^2.5.0",
"@mantine/core": "^7.16.3",
"@mantine/form": "^7.16.3",
"@mantine/hooks": "^7.16.3",
"@mantine/modals": "^7.16.3",
"@mantine/notifications": "^7.16.3",
"@mantine/spotlight": "^7.16.3",
"@lingui/core": "^5.2.0",
"@lingui/react": "^5.2.0",
"@monaco-editor/react": "^4.7.0-rc.0",
"@reduxjs/toolkit": "^2.5.1",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0",
"monaco-editor": "^0.52.2",
"mousetrap": "^1.6.5",
"react": "^18.3.1",
"react": "^19.0.0",
"react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0",
"react-device-detect": "^2.2.3",
"react-dom": "^18.3.1",
"react-dom": "^19.0.0",
"react-draggable": "^4.4.6",
"react-ga4": "^2.1.0",
"react-helmet": "^6.1.0",
"react-icons": "^5.4.0",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.2",
"react-router-dom": "^7.1.5",
"react-swipeable": "^7.0.2",
"redoc": "^2.3.0",
"redoc": "^2.4.0",
"style-to-object": "^1.0.8",
"throttle-debounce": "^5.0.2",
"tinycon": "^0.6.8",
"tss-react": "^4.9.14",
"tss-react": "^4.9.15",
"websocket-heartbeat-js": "^1.1.3"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@lingui/babel-plugin-lingui-macro": "^5.1.2",
"@lingui/cli": "^5.1.2",
"@lingui/vite-plugin": "^5.1.2",
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
"@lingui/cli": "^5.2.0",
"@lingui/vite-plugin": "^5.2.0",
"@types/mousetrap": "^1.6.15",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-helmet": "^6.1.11",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react-infinite-scroller": "^1.2.5",
"@types/swagger-ui-react": "^4.18.3",
"@types/throttle-debounce": "^5.0.2",
"@types/tinycon": "^0.6.7",
"@vitejs/plugin-react": "^4.3.4",
"babel-plugin-macros": "^3.1.0",
"jsdom": "^26.0.0",
"rollup-plugin-visualizer": "^5.13.1",
"rollup-plugin-visualizer": "^5.14.0",
"typescript": "^5.7.3",
"vite": "^6.0.7",
"vite": "^6.1.0",
"vite-plugin-checker": "^0.8.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.1",
"vitest": "^3.0.5",
"vitest-mock-extended": "^2.0.2"
},
"overrides": {
"interweave": {
"react": "^19.0.0"
},
"react-infinite-scroller": {
"react": "^19.0.0"
}
}
}

View File

@@ -6,16 +6,16 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.4.0</version>
<version>5.5.0</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
<properties>
<!-- renovate: datasource=node-version depName=node -->
<node.version>v22.13.0</node.version>
<node.version>v22.13.1</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>11.0.0</npm.version>
<npm.version>11.1.0</npm.version>
</properties>
<build>

View File

@@ -32,7 +32,6 @@ import { RegistrationPage } from "pages/auth/RegistrationPage"
import React, { useEffect } from "react"
import { isSafari } from "react-device-detect"
import ReactGA from "react-ga4"
import { Helmet } from "react-helmet"
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
import Tinycon from "tinycon"
@@ -143,7 +142,7 @@ function GoogleAnalyticsHandler() {
}
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
return <Helmet title={enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"} />
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title>
}
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
@@ -170,15 +169,6 @@ function BrowserExtensionBadgeUnreadCountHandler() {
return null
}
function CustomCode() {
return (
<Helmet>
<link rel="stylesheet" type="text/css" href="custom_css.css" />
<script type="text/javascript" src="custom_js.js" />
</Helmet>
)
}
export function App() {
useI18n()
const root = useAppSelector(state => state.tree.rootCategory)
@@ -202,7 +192,6 @@ export function App() {
<GoogleAnalyticsHandler />
<RedirectHandler />
<AppRoutes />
<CustomCode />
{/* 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

View File

@@ -1,13 +1,11 @@
import { t } from "@lingui/core/macro"
import type { IconType } from "react-icons"
import { FaAt } from "react-icons/fa"
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
import type { Category, Entry, SharingSettings } from "./types"
const categories: Record<string, Category> = {
const categories: Record<string, Omit<Category, "name">> = {
all: {
id: "all",
name: t`All`,
expanded: false,
children: [],
feeds: [],
@@ -15,7 +13,6 @@ const categories: Record<string, Category> = {
},
starred: {
id: "starred",
name: t`Starred`,
expanded: false,
children: [],
feeds: [],

View File

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

View File

@@ -1,15 +1,4 @@
import { Helmet } from "react-helmet"
export const DisablePullToRefresh = () => {
return (
<Helmet>
<style type="text/css">
{`
html, body {
overscroll-behavior: none;
}
`}
</style>
</Helmet>
)
import("./DisablePullToRefresh.css")
return <></>
}

View File

@@ -19,7 +19,7 @@ export function Enclosure(props: {
)}
{hasAudio && (
// biome-ignore lint/a11y/useMediaCaption: we don't have any captions for audio
<audio controls>
<audio controls style={{ width: "100%" }}>
<source src={props.enclosureUrl} type={props.enclosureType} />
</audio>
)}

View File

@@ -1,3 +1,5 @@
import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro"
import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"
import { useForm } from "@mantine/form"
@@ -19,6 +21,7 @@ import { useParams } from "react-router-dom"
export function CategoryDetailsPage() {
const { id = Constants.categories.all.id } = useParams()
const { _ } = useLingui()
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
const dispatch = useAppDispatch()
@@ -26,7 +29,7 @@ export function CategoryDetailsPage() {
const query = useAsync(async () => await client.category.getRoot(), [])
const category =
id === Constants.categories.starred.id
? Constants.categories.starred
? { ...Constants.categories.starred, name: _(msg`Starred`) }
: query.result && flattenCategoryTree(query.result.data).find(c => c.id === id)
const form = useForm<CategoryModificationRequest>()
@@ -63,14 +66,14 @@ export function CategoryDetailsPage() {
}
useEffect(() => {
if (!category) return
if (!category?.id) return
setValues({
id: +category.id,
name: category.name,
parentId: category.parentId,
position: category.position,
})
}, [setValues, category])
}, [setValues, category?.id, category?.name, category?.parentId, category?.position])
const editable = id !== Constants.categories.all.id && id !== Constants.categories.starred.id
if (!category) return <Loader />

View File

@@ -18,7 +18,7 @@ import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile"
import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage"
import { type ReactNode, Suspense, useEffect, useRef } from "react"
import { type ReactNode, type RefObject, Suspense, useEffect, useRef } from "react"
import Draggable from "react-draggable"
import { TbMenu2, TbPlus, TbX } from "react-icons/tb"
import { Outlet } from "react-router-dom"
@@ -185,7 +185,7 @@ export default function Layout(props: LayoutProps) {
</AppShell.Navbar>
<OnDesktop>
<Draggable
nodeRef={draggableSeparator}
nodeRef={draggableSeparator as RefObject<HTMLElement>}
axis="x"
defaultPosition={{
x: sidebarWidth,

View File

@@ -6,13 +6,13 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>5.4.0</version>
<version>5.5.0</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
<properties>
<quarkus.version>3.17.7</quarkus.version>
<quarkus.version>3.18.2</quarkus.version>
<querydsl.version>6.10.1</querydsl.version>
<rome.version>2.1.0</rome.version>
<swagger.version>2.2.28</swagger.version>
@@ -241,7 +241,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.21.1</version>
<version>10.21.2</version>
</dependency>
</dependencies>
<executions>
@@ -297,7 +297,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>5.4.0</version>
<version>5.5.0</version>
</dependency>
<!-- compile-time processors -->
@@ -453,7 +453,7 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.4.1</version>
<version>5.4.2</version>
</dependency>
<!-- add brotli support for httpclient5 -->
<dependency>
@@ -492,7 +492,7 @@
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.49.0</version>
<version>1.50.0</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -1,5 +1,7 @@
package com.commafeed;
import java.time.InstantSource;
import com.codahale.metrics.MetricRegistry;
import jakarta.enterprise.inject.Produces;
@@ -8,9 +10,16 @@ import jakarta.inject.Singleton;
@Singleton
public class CommaFeedProducers {
@Produces
@Singleton
public InstantSource instantSource() {
return InstantSource.system();
}
@Produces
@Singleton
public MetricRegistry metricRegistry() {
return new MetricRegistry();
}
}

View File

@@ -2,19 +2,16 @@ package com.commafeed.backend;
import java.io.IOException;
import java.io.InputStream;
import java.net.IDN;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.config.TlsConfig;
@@ -25,6 +22,7 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuil
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
@@ -51,6 +49,7 @@ import jakarta.ws.rs.core.CacheControl;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import nl.altindag.ssl.SSLFactory;
@@ -64,11 +63,13 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
public class HttpGetter {
private final CommaFeedConfiguration config;
private final InstantSource instantSource;
private final CloseableHttpClient client;
private final Cache<HttpRequest, HttpResponse> cache;
public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) {
public HttpGetter(CommaFeedConfiguration config, InstantSource instantSource, CommaFeedVersion version, MetricRegistry metrics) {
this.config = config;
this.instantSource = instantSource;
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config);
String userAgent = config.httpClient()
@@ -88,11 +89,11 @@ public class HttpGetter {
() -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> e.content != null ? e.content.length : 0).sum());
}
public HttpResult get(String url) throws IOException, NotModifiedException {
public HttpResult get(String url) throws IOException, NotModifiedException, TooManyRequestsException {
return get(HttpRequest.builder(url).build());
}
public HttpResult get(HttpRequest request) throws IOException, NotModifiedException {
public HttpResult get(HttpRequest request) throws IOException, NotModifiedException, TooManyRequestsException {
final HttpResponse response;
if (cache == null) {
response = invoke(request);
@@ -109,9 +110,15 @@ public class HttpGetter {
}
int code = response.getCode();
if (code == HttpStatus.SC_TOO_MANY_REQUESTS || code == HttpStatus.SC_SERVICE_UNAVAILABLE && response.getRetryAfter() != null) {
throw new TooManyRequestsException(response.getRetryAfter());
}
if (code == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("'304 - not modified' http code received");
} else if (code >= 300) {
}
if (code >= 300) {
throw new HttpResponseException(code, "Server returned HTTP error code " + code);
}
@@ -165,6 +172,12 @@ public class HttpGetter {
.map(HttpGetter::toCacheControl)
.orElse(null);
Instant retryAfter = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER))
.map(NameValuePair::getValue)
.map(StringUtils::trimToNull)
.map(this::toInstant)
.orElse(null);
String contentType = Optional.ofNullable(resp.getEntity()).map(HttpEntity::getContentType).orElse(null);
String urlAfterRedirect = Optional.ofNullable(context.getRedirectLocations())
.map(RedirectLocations::getAll)
@@ -172,7 +185,7 @@ public class HttpGetter {
.map(URI::toString)
.orElse(request.getUrl());
return new HttpResponse(code, lastModifiedHeader, eTagHeader, cacheControl, content, contentType, urlAfterRedirect);
return new HttpResponse(code, lastModifiedHeader, eTagHeader, cacheControl, retryAfter, content, contentType, urlAfterRedirect);
});
}
@@ -185,6 +198,18 @@ public class HttpGetter {
}
}
private Instant toInstant(String headerValue) {
if (headerValue == null) {
return null;
}
if (StringUtils.isNumeric(headerValue)) {
return instantSource.instant().plusSeconds(Long.parseLong(headerValue));
}
return DateUtils.parseStandardDate(headerValue);
}
private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException {
if (entity.getContentLength() > maxBytes) {
throw new IOException(
@@ -218,7 +243,6 @@ public class HttpGetter {
.setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
.setMaxConnPerRoute(poolSize)
.setMaxConnTotal(poolSize)
.setDnsResolver(new InternationalizedDomainNameToAsciiDnsResolver(SystemDefaultDnsResolver.INSTANCE))
.build();
}
@@ -255,18 +279,6 @@ public class HttpGetter {
.build();
}
private record InternationalizedDomainNameToAsciiDnsResolver(DnsResolver delegate) implements DnsResolver {
@Override
public InetAddress[] resolve(String host) throws UnknownHostException {
return delegate.resolve(IDN.toASCII(host));
}
@Override
public String resolveCanonicalHostname(String host) throws UnknownHostException {
return delegate.resolveCanonicalHostname(IDN.toASCII(host));
}
}
@Getter
public static class NotModifiedException extends Exception {
private static final long serialVersionUID = 1L;
@@ -292,6 +304,14 @@ public class HttpGetter {
}
}
@RequiredArgsConstructor
@Getter
public static class TooManyRequestsException extends Exception {
private static final long serialVersionUID = 1L;
private final Instant retryAfter;
}
@Getter
public static class HttpResponseException extends IOException {
private static final long serialVersionUID = 1L;
@@ -334,6 +354,7 @@ public class HttpGetter {
String lastModifiedHeader;
String eTagHeader;
CacheControl cacheControl;
Instant retryAfter;
byte[] content;
String contentType;
String urlAfterRedirect;

View File

@@ -13,6 +13,7 @@ import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.commafeed.backend.model.Feed;
import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode;
@@ -91,7 +92,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
return new Favicon(bytes, contentType);
}
private byte[] fetchForUser(String googleAuthKey, String userId) throws IOException, NotModifiedException {
private byte[] fetchForUser(String googleAuthKey, String userId) throws IOException, NotModifiedException, TooManyRequestsException {
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", "snippet")
.queryParam("key", googleAuthKey)
@@ -100,7 +101,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
return getter.get(uri.toString()).getContent();
}
private byte[] fetchForChannel(String googleAuthKey, String channelId) throws IOException, NotModifiedException {
private byte[] fetchForChannel(String googleAuthKey, String channelId)
throws IOException, NotModifiedException, TooManyRequestsException {
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", "snippet")
.queryParam("key", googleAuthKey)
@@ -109,7 +111,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
return getter.get(uri.toString()).getContent();
}
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId) throws IOException, NotModifiedException {
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId)
throws IOException, NotModifiedException, TooManyRequestsException {
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
.queryParam("part", "snippet")
.queryParam("key", googleAuthKey)

View File

@@ -13,6 +13,7 @@ import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpRequest;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.commafeed.backend.feed.parser.FeedParser;
import com.commafeed.backend.feed.parser.FeedParserResult;
import com.commafeed.backend.urlprovider.FeedURLProvider;
@@ -40,7 +41,8 @@ public class FeedFetcher {
}
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException {
Instant lastPublishedDate, String lastContentHash)
throws FeedException, IOException, NotModifiedException, TooManyRequestsException {
log.debug("Fetching feed {}", feedUrl);
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());

View File

@@ -4,6 +4,8 @@ import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.apache.commons.lang3.ObjectUtils;
import com.commafeed.CommaFeedConfiguration;
import jakarta.inject.Singleton;
@@ -29,6 +31,16 @@ public class FeedRefreshIntervalCalculator {
return onFetchSuccess(publishedDate, averageEntryInterval);
}
public Instant onTooManyRequests(Instant retryAfter) {
Instant defaultRefreshInterval = getDefaultRefreshInterval();
if (retryAfter == null) {
return defaultRefreshInterval;
}
return ObjectUtils.max(retryAfter, defaultRefreshInterval);
}
public Instant onFetchError(int errorCount) {
int retriesBeforeDisable = 3;
if (errorCount < retriesBeforeDisable || !empiricalInterval) {

View File

@@ -13,6 +13,7 @@ import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
import com.commafeed.backend.model.Feed;
@@ -97,6 +98,14 @@ public class FeedRefreshWorker {
feed.setEtagHeader(e.getNewEtagHeader());
}
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
} catch (TooManyRequestsException e) {
log.debug("Too many requests : {}", feed.getUrl());
feed.setErrorCount(feed.getErrorCount() + 1);
feed.setMessage("Server indicated that we are sending too many requests");
feed.setDisabledUntil(refreshIntervalCalculator.onTooManyRequests(e.getRetryAfter()));
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
} catch (Exception e) {
log.debug("unable to refresh feed {}", feed.getUrl(), e);

View File

@@ -1,7 +1,6 @@
package com.commafeed.backend.urlprovider;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import jakarta.inject.Singleton;
@@ -14,12 +13,16 @@ import jakarta.inject.Singleton;
@Singleton
public class YoutubeFeedURLProvider implements FeedURLProvider {
private static final Pattern REGEXP = Pattern.compile("(.*\\byoutube\\.com)\\/channel\\/([^\\/]+)", Pattern.CASE_INSENSITIVE);
private static final String PREFIX = "https://www.youtube.com/channel/";
private static final String REPLACEMENT_PREFIX = "https://www.youtube.com/feeds/videos.xml?channel_id=";
@Override
public String get(String url, String urlContent) {
Matcher matcher = REGEXP.matcher(url);
return matcher.find() ? matcher.group(1) + "/feeds/videos.xml?channel_id=" + matcher.group(2) : null;
if (!StringUtils.startsWithIgnoreCase(url, PREFIX)) {
return null;
}
return REPLACEMENT_PREFIX + url.substring(PREFIX.length());
}
}

View File

@@ -6,6 +6,7 @@ import java.io.OutputStream;
import java.math.BigInteger;
import java.net.SocketTimeoutException;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
@@ -39,6 +40,7 @@ import com.commafeed.CommaFeedVersion;
import com.commafeed.backend.HttpGetter.HttpResponseException;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.google.common.net.HttpHeaders;
import io.quarkus.runtime.configuration.MemorySize;
@@ -46,9 +48,12 @@ import io.quarkus.runtime.configuration.MemorySize;
@ExtendWith(MockServerExtension.class)
class HttpGetterTest {
private static final Instant NOW = Instant.now();
private MockServerClient mockServerClient;
private String feedUrl;
private byte[] feedContent;
private CommaFeedConfiguration config;
private HttpGetter getter;
@@ -73,7 +78,7 @@ class HttpGetterTest {
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
}
@ParameterizedTest
@@ -94,7 +99,8 @@ class HttpGetterTest {
.withContentType(MediaType.APPLICATION_ATOM_XML)
.withHeader(HttpHeaders.LAST_MODIFIED, "123456")
.withHeader(HttpHeaders.ETAG, "78910")
.withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60, must-revalidate"));
.withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60, must-revalidate")
.withHeader(HttpHeaders.RETRY_AFTER, "120"));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertArrayEquals(feedContent, result.getContent());
@@ -117,6 +123,27 @@ class HttpGetterTest {
Assertions.assertEquals(Duration.ZERO, result.getValidFor());
}
@Test
void tooManyRequestsExceptionSeconds() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(
HttpResponse.response().withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS).withHeader(HttpHeaders.RETRY_AFTER, "120"));
TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(NOW.plusSeconds(120), e.getRetryAfter());
}
@Test
void tooManyRequestsExceptionDate() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response()
.withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS)
.withHeader(HttpHeaders.RETRY_AFTER, "Wed, 21 Oct 2015 07:28:00 GMT"));
TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(Instant.parse("2015-10-21T07:28:00Z"), e.getRetryAfter());
}
@ParameterizedTest
@ValueSource(
ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT,
@@ -145,7 +172,7 @@ class HttpGetterTest {
@Test
void dataTimeout() {
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500));
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
@@ -156,7 +183,7 @@ class HttpGetterTest {
@Test
void connectTimeout() {
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500));
this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
// try to connect to a non-routable address
// https://stackoverflow.com/a/904609
Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.get("http://10.255.255.1"));
@@ -209,7 +236,7 @@ class HttpGetterTest {
}
@Test
void cacheSubsequentCalls() throws IOException, NotModifiedException {
void cacheSubsequentCalls() throws IOException, NotModifiedException, TooManyRequestsException {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
@@ -275,17 +302,17 @@ class HttpGetterTest {
class Compression {
@Test
void deflate() throws IOException, NotModifiedException {
void deflate() throws IOException, NotModifiedException, TooManyRequestsException {
supportsCompression("deflate", DeflaterOutputStream::new);
}
@Test
void gzip() throws IOException, NotModifiedException {
void gzip() throws IOException, NotModifiedException, TooManyRequestsException {
supportsCompression("gzip", GZIPOutputStream::new);
}
void supportsCompression(String encoding, CompressionOutputStreamFunction compressionOutputStreamFunction)
throws IOException, NotModifiedException {
throws IOException, NotModifiedException, TooManyRequestsException {
String body = "my body";
HttpGetterTest.this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {

View File

@@ -0,0 +1,22 @@
package com.commafeed.backend.urlprovider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class YoutubeFeedURLProviderTest {
private final YoutubeFeedURLProvider provider = new YoutubeFeedURLProvider();
@Test
void matchesYoutubeChannelURL() {
Assertions.assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=abc",
provider.get("https://www.youtube.com/channel/abc", null));
}
@Test
void doesNotmatchYoutubeChannelURL() {
Assertions.assertNull(provider.get("https://www.anothersite.com/channel/abc", null));
Assertions.assertNull(provider.get("https://www.youtube.com/user/abc", null));
}
}

View File

@@ -3,9 +3,10 @@ package com.commafeed.e2e;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -14,6 +15,7 @@ import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import com.commafeed.frontend.model.Entries;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
@@ -22,6 +24,7 @@ import com.microsoft.playwright.assertions.PlaywrightAssertions;
import com.microsoft.playwright.options.AriaRole;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class ReadingIT {
@@ -40,11 +43,15 @@ class ReadingIT {
.respond(HttpResponse.response()
.withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8))
.withDelay(TimeUnit.MILLISECONDS, 100));
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
playwright.close();
RestAssured.reset();
}
@Test
@@ -66,22 +73,32 @@ class ReadingIT {
main.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Next")).click();
main.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Subscribe").setExact(true)).click();
// click on subscription
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click();
// click on subscription, "2" is actually the unread count
sidebar.getByText("CommaFeed test feed2").click();
// we have two unread entries
PlaywrightAssertions.assertThat(main.locator(".mantine-Paper-root")).hasCount(2);
PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(2);
// click on first entry
main.getByText("Item 1").click();
PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(1);
PlaywrightAssertions.assertThat(main.getByText("Item 2 description")).hasCount(0);
// click on subscription
sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click();
// wait for the entry to be marked as read since the UI is updated immediately while the entry is marked as read in the background
Awaitility.await()
.atMost(15, TimeUnit.SECONDS)
.until(() -> RestAssured.given()
.get("rest/category/entries?id=all&readType=unread")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Entries.class), e -> e.getEntries().size() == 1);
// click on subscription, "1" is actually the unread count
sidebar.getByText("CommaFeed test feed1").click();
// only one unread entry now
PlaywrightAssertions.assertThat(main.locator(".mantine-Paper-root")).hasCount(1);
PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(1);
// click on second entry
main.getByText("Item 2").click();

View File

@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -41,6 +42,11 @@ class WebSocketIT extends BaseIT {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void tearDown() {
RestAssured.reset();
}
@Test
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
AtomicBoolean connected = new AtomicBoolean();

View File

@@ -3,6 +3,7 @@ package com.commafeed.integration.rest;
import java.util.List;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@@ -25,6 +26,11 @@ class AdminIT extends BaseIT {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Nested
class Users {
@Test

View File

@@ -11,6 +11,7 @@ import java.util.Objects;
import org.apache.commons.io.IOUtils;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@@ -38,6 +39,11 @@ class FeedIT extends BaseIT {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Nested
class Fetch {
@Test

View File

@@ -1,6 +1,7 @@
package com.commafeed.integration.rest;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -37,6 +38,11 @@ class FeverIT extends BaseIT {
this.userId = user.getId();
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void invalidApiKey() {
FeverResponse response = fetch("feeds", "invalid-key");

View File

@@ -2,6 +2,7 @@ package com.commafeed.integration.rest;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -29,6 +30,11 @@ class UserIT extends BaseIT {
mailbox.clear();
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void resetPassword() {
PasswordResetRequest req = new PasswordResetRequest();

View File

@@ -2,6 +2,7 @@ package com.commafeed.integration.servlet;
import org.apache.hc.core5.http.HttpStatus;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -20,6 +21,11 @@ class CustomCodeIT extends BaseIT {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void test() {
// get settings

View File

@@ -1,6 +1,7 @@
package com.commafeed.integration.servlet;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -18,6 +19,11 @@ class NextUnreadIT extends BaseIT {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void test() {
subscribeAndWaitForEntries(getFeedUrl());

View File

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

View File

@@ -4,6 +4,7 @@
"config:recommended",
"customManagers:mavenPropertyVersions",
"customManagers:biomeVersions",
"helpers:pinGitHubActionDigests",
":automergePatch",
":automergeBranch",
":automergeRequireAllStatusChecks",
@@ -31,7 +32,7 @@
"matchDatasources": "docker",
"matchPackageNames": "ibm-semeru-runtimes",
"versioning": "regex:^open-(?<major>\\d+)?(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?([\\._+](?<build>(\\d\\.?)+))?(-(?<compatibility>.*))?$",
"allowedVersions": "/^open-(?:8|11|17|21)(?:\\.|-|$)/"
"allowedVersions": "/^open-(?:8|11|17|21|25)(?:\\.|-|$)/"
}
]
}