Compare commits

..

12 Commits

Author SHA1 Message Date
Athou
06319c1eb0 release 3.10.0 2023-09-06 09:04:21 +02:00
Athou
b7ede8eba2 add instructions for the Fever API 2023-09-05 11:04:52 +02:00
Athou
1a4517d6a3 add support for FeedMe 2023-09-05 11:04:52 +02:00
Athou
a402c5d7d8 add support for FocusReader 2023-09-05 11:04:52 +02:00
Athou
408809787e add support for Raven Reader 2023-09-05 11:04:52 +02:00
Athou
d7b0d572c1 add fever-compatible api 2023-09-05 11:04:52 +02:00
Athou
b356be3e6f show the whole title in the detailed view (#1097 #1144) 2023-09-05 09:10:26 +02:00
Athou
998385334b add metric for deleted entries 2023-09-03 12:16:43 +02:00
Athou
c6d613d81a add "s" keyboard shortcut to star/unstar entries (#1142) 2023-08-27 11:43:30 +02:00
Athou
9981d8763d don't set default values for env variables (#1141) 2023-08-24 07:51:10 +02:00
Athou
b37680333c clean database after each test 2023-08-23 20:36:57 +02:00
Athou
66d1eb3f1f store sessions in database 2023-08-23 20:34:29 +02:00
54 changed files with 1092 additions and 80 deletions

View File

@@ -1,5 +1,13 @@
# Changelog
## [3.10.0]
- added a Fever-compatible API that is usable with mobile clients that support the Fever API (see instructions in Settings -> Profile)
- long entry titles are no longer shortened in the detailed view
- added the "s" keyboard shortcut to star/unstar entries
- http sessions are now stored in the database (they were stored on disk before)
- fixed an issue that made it impossible to override the database url in a config.yml mounted in the Docker image
## [3.9.0]
- improve performance by disabling the loader when nothing is loading (most noticeable on mobile)

View File

@@ -4,8 +4,6 @@ EXPOSE 8082
RUN mkdir -p /commafeed/data
VOLUME /commafeed/data
ENV CF_SESSION_PATH=/commafeed/data/sessions
ENV CF_DATABASE_URL=jdbc:h2:/commafeed/data/db
COPY commafeed-server/config.yml.example config.yml
COPY commafeed-server/target/commafeed.jar .

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>3.9.0</version>
<version>3.10.0</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>

View File

@@ -113,6 +113,14 @@ export function KeyboardShortcutsHelp() {
<Trans>Swipe header to the right</Trans>
</td>
</tr>
<tr>
<td>
<Trans>Toggle starred status of current entry</Trans>
</td>
<td>
<Kbd>S</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Mark all entries as read</Trans>

View File

@@ -11,6 +11,7 @@ import {
selectEntry,
selectNextEntry,
selectPreviousEntry,
starEntry,
} from "app/slices/entries"
import { redirectToRootCategory } from "app/slices/redirect"
import { toggleSidebar } from "app/slices/tree"
@@ -257,6 +258,11 @@ export function FeedEntries() {
if (!selectedEntry) return
dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read }))
})
useMousetrap("s", () => {
// toggle starred status
if (!selectedEntry) return
dispatch(starEntry({ entry: selectedEntry, starred: !selectedEntry.starred }))
})
useMousetrap("shift+a", () => {
// mark all entries as read
dispatch(

View File

@@ -1,4 +1,4 @@
import { Box, createStyles, Text } from "@mantine/core"
import { Box, createStyles, Space, Text } from "@mantine/core"
import { Entry } from "app/types"
import { RelativeDate } from "components/RelativeDate"
import { FeedEntryTitle } from "./FeedEntryTitle"
@@ -12,17 +12,11 @@ export interface FeedEntryHeaderProps {
const useStyles = createStyles((theme, props: FeedEntryHeaderProps) => ({
headerText: {
fontWeight: theme.colorScheme === "light" && !props.entry.read ? "bold" : "inherit",
whiteSpace: props.expanded ? "inherit" : "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
},
headerSubtext: {
display: "flex",
alignItems: "center",
fontSize: "90%",
whiteSpace: props.expanded ? "inherit" : "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
},
}))
export function FeedEntryHeader(props: FeedEntryHeaderProps) {
@@ -33,18 +27,13 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
<FeedEntryTitle entry={props.entry} />
</Box>
<Box className={classes.headerSubtext}>
<Box mr={6}>
<FeedFavicon url={props.entry.iconUrl} />
</Box>
<Box>
<Text color="dimmed">{props.entry.feedName}</Text>
</Box>
<Box>
<Text color="dimmed">
<span>&nbsp;·&nbsp;</span>
<RelativeDate date={props.entry.date} />
</Text>
</Box>
<FeedFavicon url={props.entry.iconUrl} />
<Space w={6} />
<Text color="dimmed">
{props.entry.feedName}
<span> · </span>
<RelativeDate date={props.entry.date} />
</Text>
</Box>
{props.expanded && (
<Box className={classes.headerSubtext}>

View File

@@ -77,10 +77,7 @@ export function ProfileSettings() {
<form onSubmit={form.onSubmit(saveProfile.execute)}>
<Stack>
<Input.Wrapper label={<Trans>User name</Trans>}>
<Box>{profile?.name}</Box>
</Input.Wrapper>
<TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
<TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} />
<Input.Wrapper
@@ -98,6 +95,22 @@ export function ProfileSettings() {
</Box>
</Input.Wrapper>
<Input.Wrapper
label={<Trans>Fever API</Trans>}
description={
<Trans>
CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client.
The username is your user name and the password is your API key.
</Trans>
}
>
<Box>
<Anchor href={`rest/fever/user/${profile?.id}`} target="_blank">
<Trans>Fever API URL</Trans>
</Anchor>
</Box>
</Input.Wrapper>
<Divider />
<PasswordInput

View File

@@ -179,6 +179,10 @@ msgstr "تأكد من عمل الخلاصة"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء"
@@ -343,6 +347,14 @@ msgstr "موجز URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "الملف مطلوب"
@@ -832,6 +844,10 @@ msgstr "تبديل قراءة حالة الإدخال الحالي"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"

View File

@@ -179,6 +179,10 @@ msgstr "Comproveu que el canal funciona"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed següent element no llegit"
@@ -343,6 +347,14 @@ msgstr "URL del canal"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "el fitxer és necessari"
@@ -832,6 +844,10 @@ msgstr "Canvia l'estat de lectura de l'entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Zkontrolujte, zda zdroj funguje"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka"
@@ -343,6 +347,14 @@ msgstr "URL zdroje"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr ""
@@ -832,6 +844,10 @@ msgstr "Přepne stav čtení aktuálního záznamu"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Gwiriwch fod y porthiant yn gweithio"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen"
@@ -343,6 +347,14 @@ msgstr "URL porthiant"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "mae angen y ffeil"
@@ -832,6 +844,10 @@ msgstr "Toglo statws darllen y cofnod cyfredol"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"

View File

@@ -179,6 +179,10 @@ msgstr "Tjek, at foderet virker"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element"
@@ -343,6 +347,14 @@ msgstr ""
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil er påkrævet"
@@ -832,6 +844,10 @@ msgstr "Skift læsestatus for den aktuelle post"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Überprüfen Sie, ob der Feed funktioniert"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element"
@@ -343,6 +347,14 @@ msgstr "Feed-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "Datei ist erforderlich"
@@ -832,6 +844,10 @@ msgstr "Lesestatus des aktuellen Eintrags umschalten"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Check that the feed is working"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item"
@@ -343,6 +347,14 @@ msgstr "Feed URL"
msgid "Fetch all my feeds now"
msgstr "Fetch all my feeds now"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr "Fever API"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "Fever API URL"
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file is required"
@@ -832,6 +844,10 @@ msgstr "Toggle read status of current entry"
msgid "Toggle sidebar"
msgstr "Toggle sidebar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Toggle starred status of current entry"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Try out CommaFeed with the demo account: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Compruebe que el feed funciona"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído"
@@ -343,6 +347,14 @@ msgstr "URL de fuente"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "archivo requerido"
@@ -832,6 +844,10 @@ msgstr "Alternar estado de lectura de la entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "بررسی کنید که خوراک کار می کند"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed"
@@ -343,6 +347,14 @@ msgstr "URL فید"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "فایل مورد نیاز است"
@@ -832,6 +844,10 @@ msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"

View File

@@ -179,6 +179,10 @@ msgstr "Tarkista, että syöttö toimii"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde"
@@ -343,6 +347,14 @@ msgstr "Syötteen URL-osoite"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "tiedosto vaaditaan"
@@ -832,6 +844,10 @@ msgstr "Vaihda nykyisen merkinnän lukutila"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Vérifie que le flux fonctionne"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu"
@@ -343,6 +347,14 @@ msgstr "URL du flux"
msgid "Fetch all my feeds now"
msgstr "Rafraîchir tous mes flux"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fichier requis"
@@ -832,6 +844,10 @@ msgstr "Marquer l'entrée actuelle comme lue/non lue"
msgid "Toggle sidebar"
msgstr "Montrer/cacher la barre latérale"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Comproba que a fonte funciona"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed seguinte elemento non lido"
@@ -343,6 +347,14 @@ msgstr "URL da fonte"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "é necesario o ficheiro"
@@ -832,6 +844,10 @@ msgstr "alternar o estado de lectura da entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Ellenőrizze, hogy a feed működik-e"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem"
@@ -343,6 +347,14 @@ msgstr ""
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fájl szükséges"
@@ -832,6 +844,10 @@ msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Periksa apakah umpannya berfungsi"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya"
@@ -343,6 +347,14 @@ msgstr "URL Umpan"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file diperlukan"
@@ -832,6 +844,10 @@ msgstr "Beralih status baca entri saat ini"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Verifica che il feed funzioni"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto"
@@ -343,6 +347,14 @@ msgstr "URL feed"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "è richiesto il file"
@@ -832,6 +844,10 @@ msgstr "Commuta lo stato di lettura della voce corrente"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed con il conto demo: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "フィードが動作していることを確認してください"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "次の未読アイテムをカンマフィード"
@@ -343,6 +347,14 @@ msgstr "フィード URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "ファイルが必要です"
@@ -832,6 +844,10 @@ msgstr "現在のエントリの読み取りステータスを切り替えます
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "デモアカウントで CommaFeed を試す: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "피드가 작동하는지 확인"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "다음 읽지 않은 항목을 쉼표로 피드"
@@ -343,6 +347,14 @@ msgstr "피드 URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "파일이 필요합니다"
@@ -832,6 +844,10 @@ msgstr "현재 항목의 읽기 상태 전환"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Semak sama ada suapan berfungsi"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed item belum dibaca seterusnya"
@@ -343,6 +347,14 @@ msgstr "URL Suapan"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fail diperlukan"
@@ -832,6 +844,10 @@ msgstr "Togol status bacaan entri semasa"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Sjekk at feeden fungerer"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element"
@@ -343,6 +347,14 @@ msgstr "Feed-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil kreves"
@@ -832,6 +844,10 @@ msgstr "Veksle lesestatus for gjeldende oppføring"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Controleer of de feed werkt"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed volgende ongelezen item"
@@ -343,6 +347,14 @@ msgstr "Feed-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "bestand is vereist"
@@ -832,6 +844,10 @@ msgstr "Toggle leesstatus van huidige invoer"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Sjekk at feeden fungerer"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element"
@@ -343,6 +347,14 @@ msgstr "Feed-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil kreves"
@@ -832,6 +844,10 @@ msgstr "Veksle lesestatus for gjeldende oppføring"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Sprawdź, czy kanał działa"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "Przecinek następny nieprzeczytany element"
@@ -343,6 +347,14 @@ msgstr "URL kanału"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "plik jest wymagany"
@@ -832,6 +844,10 @@ msgstr "Przełącz stan odczytu bieżącego wpisu"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Verifique se o feed está funcionando"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed próximo item não lido"
@@ -343,6 +347,14 @@ msgstr "URL do feed"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "o arquivo é obrigatório"
@@ -832,6 +844,10 @@ msgstr "Alternar o status de leitura da entrada atual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Experimente o CommaFeed com a conta demo: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Проверьте, работает ли лента."
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed следующий непрочитанный элемент"
@@ -343,6 +347,14 @@ msgstr "URL-адрес фида"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "требуется файл"
@@ -832,6 +844,10 @@ msgstr "Переключить статус чтения текущей запи
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Skontrolujte, či feed funguje"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed ďalšia neprečítaná položka"
@@ -343,6 +347,14 @@ msgstr "URL informačného kanála"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr ""
@@ -832,6 +844,10 @@ msgstr "Prepne stav čítania aktuálneho záznamu"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Kontrollera att matningen fungerar"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed nästa olästa objekt"
@@ -343,6 +347,14 @@ msgstr "Flödes-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil krävs"
@@ -832,6 +844,10 @@ msgstr "Växla lässtatus för aktuell post"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed med demokontot: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "Feed'in çalışıp çalışmadığını kontrol edin"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed sonraki okunmamış öğe"
@@ -343,6 +347,14 @@ msgstr "Feed URL'si"
msgid "Fetch all my feeds now"
msgstr "Tüm feed'lerimi şimdi çek"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "dosya gerekli"
@@ -832,6 +844,10 @@ msgstr "Geçerli girişin okuma durumunu değiştir"
msgid "Toggle sidebar"
msgstr "Kenar çubuğunu göster/gizle"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"

View File

@@ -179,6 +179,10 @@ msgstr "检查提要是否正常工作"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed 下一个未读项目"
@@ -343,6 +347,14 @@ msgstr "供稿网址"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "文件是必需的"
@@ -832,6 +844,10 @@ msgstr "切换当前条目的读取状态"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "使用演示帐户试用 CommaFeeddemo/demo"

View File

@@ -14,6 +14,7 @@ const shownMeters: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
"com.commafeed.backend.service.DatabaseCleaningService.entriesDeleted": "Entries deleted",
}
const shownGauges: { [key: string]: string } = {

View File

@@ -103,7 +103,7 @@ app:
database:
driverClass: org.h2.Driver
url: jdbc:h2:./db/commafeed
url: jdbc:h2:/commafeed/data/db
user: sa
password: sa
properties:

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>3.9.0</version>
<version>3.10.0</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
@@ -232,7 +232,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>3.9.0</version>
<version>3.10.0</version>
</dependency>
<dependency>

View File

@@ -42,6 +42,7 @@ import com.commafeed.frontend.resource.FeedREST;
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
import com.commafeed.frontend.resource.ServerREST;
import com.commafeed.frontend.resource.UserREST;
import com.commafeed.frontend.resource.fever.FeverREST;
import com.commafeed.frontend.servlet.AnalyticsServlet;
import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.CustomJsServlet;
@@ -147,7 +148,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
// session management
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build());
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
// support for "@SecurityCheck User user" injection
environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class)));
@@ -163,6 +164,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.jersey().register(injector.getInstance(PubSubHubbubCallbackREST.class));
environment.jersey().register(injector.getInstance(ServerREST.class));
environment.jersey().register(injector.getInstance(UserREST.class));
environment.jersey().register(injector.getInstance(FeverREST.class));
// Servlets
environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next");

View File

@@ -117,7 +117,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
private JPAQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords,
Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag) {
Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId, Long maxEntryId) {
JPAQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed()));
@@ -159,6 +159,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
query.where(entry.inserted.goe(newerThan));
}
if (minEntryId != null) {
query.where(entry.id.gt(minEntryId));
}
if (maxEntryId != null) {
query.where(entry.id.lt(maxEntryId));
}
if (last != null) {
if (order == ReadingOrder.desc) {
query.where(entry.updated.gt(last.getEntryUpdated()));
@@ -189,7 +197,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
boolean onlyIds, String tag) {
boolean onlyIds, String tag, Long minEntryId, Long maxEntryId) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
@@ -197,7 +205,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<>(capacity, comparator);
for (FeedSubscription sub : subs) {
FeedEntryStatus last = (order != null && set.isFull()) ? set.last() : null;
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag, minEntryId,
maxEntryId);
List<Tuple> tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch();
for (Tuple tuple : tuples) {
@@ -250,7 +259,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
UnreadCount uc = null;
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null);
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null, null, null);
List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch();
for (Tuple tuple : tuples) {
Long count = tuple.get(entry.count());

View File

@@ -6,6 +6,8 @@ import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO;
@@ -32,16 +34,18 @@ public class DatabaseCleaningService {
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryContentDAO feedEntryContentDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final Meter entriesDeletedMeter;
@Inject
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO) {
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) {
this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO;
this.feedEntryDAO = feedEntryDAO;
this.feedEntryContentDAO = feedEntryContentDAO;
this.feedEntryStatusDAO = feedEntryStatusDAO;
this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize();
this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted"));
}
public void cleanFeedsWithoutSubscriptions() {
@@ -55,6 +59,7 @@ public class DatabaseCleaningService {
long entriesDeleted;
do {
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
entriesDeletedMeter.mark(entriesDeleted);
entriesTotal += entriesDeleted;
log.info("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0);
@@ -91,6 +96,7 @@ public class DatabaseCleaningService {
do {
final long rem = remaining;
int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(batchSize, rem)));
entriesDeletedMeter.mark(deleted);
total += deleted;
remaining -= deleted;
log.info("removed {} entries for feeds exceeding capacity", total);

View File

@@ -111,7 +111,7 @@ public class FeedEntryService {
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan, List<FeedEntryKeyword> keywords) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null,
false, false, null);
false, false, null, null, null);
markList(statuses, olderThan);
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(user);

View File

@@ -82,6 +82,28 @@ public class UserService {
return Optional.empty();
}
/**
* try to log in with given fever api key
*/
public Optional<User> login(long userId, String feverApiKey) {
if (feverApiKey == null) {
return Optional.empty();
}
User user = userDAO.findById(userId);
if (user == null || user.isDisabled() || user.getApiKey() == null) {
return Optional.empty();
}
String computedFeverApiKey = DigestUtils.md5Hex(user.getName() + ":" + user.getApiKey());
if (!computedFeverApiKey.equals(feverApiKey)) {
return Optional.empty();
}
performPostLoginActivities(user);
return Optional.of(user);
}
/**
* should triggers after successful login
*/

View File

@@ -141,7 +141,7 @@ public class CategoryREST {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag);
offset, limit + 1, order, true, onlyIds, tag, null, null);
for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
@@ -160,7 +160,7 @@ public class CategoryREST {
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag);
offset, limit + 1, order, true, onlyIds, tag, null, null);
for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));

View File

@@ -172,7 +172,7 @@ public class FeedREST {
entries.setFeedLink(subscription.getFeed().getLink());
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly,
entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null);
entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null, null, null);
for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));

View File

@@ -0,0 +1,320 @@
package com.commafeed.frontend.resource.fever;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import com.codahale.metrics.annotation.Timed;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverFavicon;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeed;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeedGroup;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverGroup;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem;
import io.dropwizard.hibernate.UnitOfWork;
import lombok.RequiredArgsConstructor;
/**
* Fever-compatible API
*
* <ul>
* <li>url: /rest/fever/user/${userId}</li>
* <li>login: username</li>
* <li>password: api key</li>
* </ul>
*
* See https://feedafever.com/api
*/
@Path("/fever")
@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FeverREST {
private static final String PATH = "/user/{userId}{optionalTrailingFever : (/fever)?}{optionalTrailingSlash : (/)?}";
private static final int UNREAD_ITEM_IDS_BATCH_SIZE = 1000;
private static final int SAVED_ITEM_IDS_BATCH_SIZE = 1000;
private static final int ITEMS_BATCH_SIZE = 200;
private final UserService userService;
private final FeedEntryService feedEntryService;
private final FeedService feedService;
private final FeedEntryDAO feedEntryDAO;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedCategoryDAO feedCategoryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
// expected Fever API
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path(PATH)
@POST
@UnitOfWork
@Timed
public FeverResponse formUrlencoded(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap<String, String> form) {
Map<String, String> params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
form.forEach((k, v) -> params.put(k, v.get(0)));
return handle(userId, params);
}
// workaround for some readers that post data without any media type, and all params in the url
// e.g. FeedMe
@Path(PATH)
@POST
@UnitOfWork
@Timed
public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) {
Map<String, String> params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
return handle(userId, params);
}
// workaround for some readers that post data using MultiPart FormData instead of the classic POST
// e.g. Raven Reader
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path(PATH)
@POST
@UnitOfWork
@Timed
public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, FormDataMultiPart form) {
Map<String, String> params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
form.getFields().forEach((k, v) -> params.put(k, v.get(0).getValue()));
return handle(userId, params);
}
public FeverResponse handle(long userId, Map<String, String> params) {
User user = auth(userId, params.get("api_key")).orElse(null);
if (user == null) {
FeverResponse resp = new FeverResponse();
resp.setAuth(false);
return resp;
}
FeverResponse resp = new FeverResponse();
resp.setAuth(true);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
resp.setLastRefreshedOnTime(buildLastRefreshedOnTime(subscriptions));
if (params.containsKey("groups") || params.containsKey("feeds")) {
resp.setFeedsGroups(buildFeedsGroups(subscriptions));
if (params.containsKey("groups")) {
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
resp.setGroups(buildGroups(categories));
}
if (params.containsKey("feeds")) {
resp.setFeeds(buildFeeds(subscriptions));
}
}
if (params.containsKey("unread_item_ids")) {
resp.setUnreadItemIds(buildUnreadItemIds(user, subscriptions));
}
if (params.containsKey("saved_item_ids")) {
resp.setSavedItemIds(buildSavedItemIds(user));
}
if (params.containsKey("items")) {
if (params.containsKey("with_ids")) {
String withIds = params.get("with_ids");
List<String> entryIds = Stream.of(withIds.split(",")).map(String::trim).collect(Collectors.toList());
resp.setItems(buildItems(user, subscriptions, entryIds));
} else {
Long sinceId = params.containsKey("since_id") ? Long.valueOf(params.get("since_id")) : null;
Long maxId = params.containsKey("max_id") ? Long.valueOf(params.get("max_id")) : null;
resp.setItems(buildItems(user, subscriptions, sinceId, maxId));
}
}
if (params.containsKey("favicons")) {
resp.setFavicons(buildFavicons(subscriptions));
}
if (params.containsKey("links")) {
resp.setLinks(Collections.emptyList());
}
if (params.containsKey("mark") && params.containsKey("id") && params.containsKey("as")) {
long id = Long.parseLong(params.get("id"));
String before = params.get("before");
Date olderThan = before == null ? null : Date.from(Instant.ofEpochSecond(Long.parseLong(before)));
mark(user, params.get("mark"), id, params.get("as"), olderThan);
}
return resp;
}
private Optional<User> auth(Long userId, String feverApiKey) {
return userService.login(userId, feverApiKey);
}
private long buildLastRefreshedOnTime(List<FeedSubscription> subscriptions) {
return subscriptions.stream()
.map(FeedSubscription::getFeed)
.map(Feed::getLastUpdated)
.filter(Objects::nonNull)
.max(Comparator.naturalOrder())
.map(d -> d.toInstant().getEpochSecond())
.orElse(0L);
}
private List<FeverFeedGroup> buildFeedsGroups(List<FeedSubscription> subscriptions) {
return subscriptions.stream()
.collect(Collectors.groupingBy(s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
.entrySet()
.stream()
.map(e -> {
FeverFeedGroup fg = new FeverFeedGroup();
fg.setGroupId(e.getKey());
fg.setFeedIds(e.getValue().stream().map(FeedSubscription::getId).collect(Collectors.toList()));
return fg;
})
.collect(Collectors.toList());
}
private List<FeverGroup> buildGroups(List<FeedCategory> categories) {
return categories.stream().map(c -> {
FeverGroup g = new FeverGroup();
g.setId(c.getId());
g.setTitle(c.getName());
return g;
}).collect(Collectors.toList());
}
private List<FeverFeed> buildFeeds(List<FeedSubscription> subscriptions) {
return subscriptions.stream().map(s -> {
FeverFeed f = new FeverFeed();
f.setId(s.getId());
f.setFaviconId(s.getId());
f.setTitle(s.getTitle());
f.setUrl(s.getFeed().getUrl());
f.setSiteUrl(s.getFeed().getLink());
f.setSpark(false);
f.setLastUpdatedOnTime(s.getFeed().getLastUpdated() == null ? 0 : s.getFeed().getLastUpdated().toInstant().getEpochSecond());
return f;
}).collect(Collectors.toList());
}
private List<Long> buildUnreadItemIds(User user, List<FeedSubscription> subscriptions) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0,
UNREAD_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false, true, null, null, null);
return statuses.stream().map(s -> s.getEntry().getId()).collect(Collectors.toList());
}
private List<Long> buildSavedItemIds(User user) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, 0, SAVED_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false);
return statuses.stream().map(s -> s.getEntry().getId()).collect(Collectors.toList());
}
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, List<String> entryIds) {
List<FeverItem> items = new ArrayList<>();
Map<Long, FeedSubscription> subscriptionsByFeedId = subscriptions.stream()
.collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s));
for (String entryId : entryIds) {
FeedEntry entry = feedEntryDAO.findById(Long.parseLong(entryId));
FeedSubscription sub = subscriptionsByFeedId.get(entry.getFeed().getId());
if (sub != null) {
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
items.add(mapStatus(status));
}
}
return items;
}
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, false, null, null, 0, ITEMS_BATCH_SIZE,
ReadingOrder.desc, false, false, null, sinceId, maxId);
return statuses.stream().map(this::mapStatus).collect(Collectors.toList());
}
private FeverItem mapStatus(FeedEntryStatus s) {
FeverItem i = new FeverItem();
i.setId(s.getEntry().getId());
i.setFeedId(s.getSubscription().getId());
i.setTitle(s.getEntry().getContent().getTitle());
i.setAuthor(s.getEntry().getContent().getAuthor());
i.setHtml(Optional.ofNullable(s.getEntry().getContent().getContent()).orElse(""));
i.setUrl(s.getEntry().getUrl());
i.setSaved(s.isStarred());
i.setRead(s.isRead());
i.setCreatedOnTime(s.getEntryUpdated().toInstant().getEpochSecond());
return i;
}
private List<FeverFavicon> buildFavicons(List<FeedSubscription> subscriptions) {
return subscriptions.stream().map(s -> {
Favicon favicon = feedService.fetchFavicon(s.getFeed());
FeverFavicon f = new FeverFavicon();
f.setId(s.getFeed().getId());
f.setData(String.format("data:%s;base64,%s", favicon.getMediaType(), Base64.getEncoder().encodeToString(favicon.getIcon())));
return f;
}).collect(Collectors.toList());
}
private void mark(User user, String source, long id, String action, Date olderThan) {
if ("item".equals(source)) {
if ("read".equals(action) || "unread".equals(action)) {
feedEntryService.markEntry(user, id, "read".equals(action));
} else if ("saved".equals(action) || "unsaved".equals(action)) {
FeedEntry entry = feedEntryDAO.findById(id);
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action));
}
} else if ("feed".equals(source)) {
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, null);
} else if ("group".equals(source)) {
FeedCategory parent = feedCategoryDAO.findById(user, id);
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, categories);
feedEntryService.markSubscriptionEntries(user, subscriptions, olderThan, null);
}
}
}

View File

@@ -0,0 +1,164 @@
package com.commafeed.frontend.resource.fever;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
@JsonInclude(Include.NON_NULL)
@Data
public class FeverResponse {
@JsonProperty("api_version")
private int apiVersion = 3;
@JsonProperty("auth")
@JsonFormat(shape = Shape.NUMBER)
private boolean auth;
@JsonProperty("last_refreshed_on_time")
private long lastRefreshedOnTime;
@JsonProperty("groups")
private List<FeverGroup> groups;
@JsonProperty("feeds")
private List<FeverFeed> feeds;
@JsonProperty("feeds_groups")
private List<FeverFeedGroup> feedsGroups;
@JsonProperty("unread_item_ids")
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
private List<Long> unreadItemIds;
@JsonProperty("saved_item_ids")
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
private List<Long> savedItemIds;
@JsonProperty("items")
private List<FeverItem> items;
@JsonProperty("favicons")
private List<FeverFavicon> favicons;
@JsonProperty("links")
private List<FeverLink> links;
@Data
public static class FeverGroup {
@JsonProperty("id")
private long id;
@JsonProperty("title")
private String title;
}
@Data
public static class FeverFeed {
@JsonProperty("id")
private long id;
@JsonProperty("favicon_id")
private long faviconId;
@JsonProperty("title")
private String title;
@JsonProperty("url")
private String url;
@JsonProperty("site_url")
private String siteUrl;
@JsonProperty("is_spark")
@JsonFormat(shape = Shape.NUMBER)
private boolean spark;
@JsonProperty("last_updated_on_time")
private long lastUpdatedOnTime;
}
@Data
public static class FeverFeedGroup {
@JsonProperty("group_id")
private long groupId;
@JsonProperty("feed_ids")
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
private List<Long> feedIds;
}
@Data
public static class FeverItem {
@JsonProperty("id")
private long id;
@JsonProperty("feed_id")
private long feedId;
@JsonProperty("title")
private String title;
@JsonProperty("author")
private String author;
@JsonProperty("html")
private String html;
@JsonProperty("url")
private String url;
@JsonProperty("is_saved")
@JsonFormat(shape = Shape.NUMBER)
private boolean saved;
@JsonProperty("is_read")
@JsonFormat(shape = Shape.NUMBER)
private boolean read;
@JsonProperty("created_on_time")
private long createdOnTime;
}
@Data
public static class FeverFavicon {
@JsonProperty("id")
private long id;
@JsonProperty("data")
private String data;
}
@Data
public static class FeverLink {
}
public static class LongListToCommaSeparatedStringSerializer extends JsonSerializer<List<Long>> {
@Override
public void serialize(List<Long> input, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
String output = input.stream().map(String::valueOf).collect(Collectors.joining(","));
jsonGenerator.writeObject(output);
}
}
}

View File

@@ -69,7 +69,7 @@ public class NextUnreadServlet extends HttpServlet {
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get());
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order,
true, false, null);
true, false, null, null, null);
s = Iterables.getFirst(statuses, null);
} else {
FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId));
@@ -77,7 +77,7 @@ public class NextUnreadServlet extends HttpServlet {
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user.get(), category);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children);
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0,
1, order, true, false, null);
1, order, true, false, null, null, null);
s = Iterables.getFirst(statuses, null);
}
}

View File

@@ -1,24 +1,22 @@
package com.commafeed.frontend.session;
import java.io.File;
import javax.servlet.SessionTrackingMode;
import org.eclipse.jetty.server.session.DatabaseAdaptor;
import org.eclipse.jetty.server.session.DefaultSessionCache;
import org.eclipse.jetty.server.session.FileSessionDataStore;
import org.eclipse.jetty.server.session.JDBCSessionDataStore;
import org.eclipse.jetty.server.session.SessionCache;
import org.eclipse.jetty.server.session.SessionHandler;
import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableSet;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.util.Duration;
public class SessionHandlerFactory {
@JsonProperty
private String path = "sessions";
@JsonProperty
private Duration cookieMaxAge = Duration.days(30);
@@ -31,26 +29,24 @@ public class SessionHandlerFactory {
@JsonProperty
private Duration savePeriod = Duration.minutes(5);
public SessionHandler build() {
SessionHandler sessionHandler = new SessionHandler() {
{
// no setter available for maxCookieAge
_maxCookieAge = (int) cookieMaxAge.toSeconds();
}
};
SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
sessionHandler.setSessionCache(sessionCache);
FileSessionDataStore dataStore = new FileSessionDataStore();
sessionCache.setSessionDataStore(dataStore);
public SessionHandler build(DataSourceFactory dataSourceFactory) {
SessionHandler sessionHandler = new SessionHandler();
sessionHandler.setHttpOnly(true);
sessionHandler.setSessionTrackingModes(ImmutableSet.of(SessionTrackingMode.COOKIE));
sessionHandler.setMaxInactiveInterval((int) maxInactiveInterval.toSeconds());
sessionHandler.setRefreshCookieAge((int) cookieRefreshAge.toSeconds());
sessionHandler.getSessionCookieConfig().setMaxAge((int) cookieMaxAge.toSeconds());
dataStore.setDeleteUnrestorableFiles(true);
dataStore.setStoreDir(new File(path));
SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
sessionHandler.setSessionCache(sessionCache);
JDBCSessionDataStore dataStore = new JDBCSessionDataStore();
dataStore.setSavePeriodSec((int) savePeriod.toSeconds());
sessionCache.setSessionDataStore(dataStore);
DatabaseAdaptor adaptor = new DatabaseAdaptor();
adaptor.setDatasource(dataSourceFactory.build(new MetricRegistry(), "sessions"));
dataStore.setDatabaseAdaptor(adaptor);
return sessionHandler;
}

View File

@@ -0,0 +1,34 @@
package com.commafeed;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.codahale.metrics.MetricRegistry;
import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit5.DropwizardAppExtension;
public class CommaFeedDropwizardAppExtension extends DropwizardAppExtension<CommaFeedConfiguration> {
public CommaFeedDropwizardAppExtension() {
super(CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"));
}
@Override
public void after() {
super.after();
// clean database after each test
DataSource dataSource = getConfiguration().getDataSourceFactory().build(new MetricRegistry(), "cleanup");
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("DROP ALL OBJECTS")) {
statement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("could not cleanup database", e);
}
}
}

View File

@@ -3,20 +3,16 @@ package com.commafeed.e2e;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedDropwizardAppExtension;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.assertions.PlaywrightAssertions;
import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit5.DropwizardAppExtension;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
@ExtendWith(DropwizardExtensionsSupport.class)
class AuthentificationIT extends PlaywrightTestBase {
private static final DropwizardAppExtension<CommaFeedConfiguration> EXT = new DropwizardAppExtension<>(CommaFeedApplication.class,
ResourceHelpers.resourceFilePath("config.test.yml"));
private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension();
@Test
void loginFail() {

View File

@@ -12,22 +12,18 @@ import org.mockserver.junit.jupiter.MockServerExtension;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedDropwizardAppExtension;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Locator.WaitForOptions;
import com.microsoft.playwright.assertions.PlaywrightAssertions;
import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit5.DropwizardAppExtension;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
@ExtendWith(DropwizardExtensionsSupport.class)
@ExtendWith(MockServerExtension.class)
class ReadingIT extends PlaywrightTestBase {
private static final DropwizardAppExtension<CommaFeedConfiguration> EXT = new DropwizardAppExtension<CommaFeedConfiguration>(
CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"));
private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension();
private MockServerClient mockServerClient;

View File

@@ -22,21 +22,17 @@ import org.mockserver.junit.jupiter.MockServerExtension;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedDropwizardAppExtension;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.request.SubscribeRequest;
import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit5.DropwizardAppExtension;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
@ExtendWith(DropwizardExtensionsSupport.class)
@ExtendWith(MockServerExtension.class)
class FeedIT {
private static final DropwizardAppExtension<CommaFeedConfiguration> EXT = new DropwizardAppExtension<CommaFeedConfiguration>(
CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml")) {
private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension() {
@Override
protected JerseyClientBuilder clientBuilder() {
HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic("admin", "admin");

View File

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