diff --git a/README.md b/README.md
index 930aa098..902dcb44 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc
- REST API
- Fever-compatible API for native mobile apps
- Can automatically mark articles as read based on user-defined rules
+- Push notifications when new articles are published
- Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
- Compiles to native code for blazing fast startup and low memory usage
diff --git a/commafeed-client/package-lock.json b/commafeed-client/package-lock.json
index 527a51f3..73196820 100644
--- a/commafeed-client/package-lock.json
+++ b/commafeed-client/package-lock.json
@@ -6298,7 +6298,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
diff --git a/commafeed-client/src/app/tree/tree.test.ts b/commafeed-client/src/app/tree/tree.test.ts
index 7edd32e2..1a3620bc 100644
--- a/commafeed-client/src/app/tree/tree.test.ts
+++ b/commafeed-client/src/app/tree/tree.test.ts
@@ -27,6 +27,7 @@ const createFeed = (id: number, unread: number): Subscription => ({
feedUrl: "",
feedLink: "",
iconUrl: "",
+ pushNotificationsEnabled: true,
})
const root = createCategory("root")
diff --git a/commafeed-client/src/app/types.ts b/commafeed-client/src/app/types.ts
index f425a897..dba79a7d 100644
--- a/commafeed-client/src/app/types.ts
+++ b/commafeed-client/src/app/types.ts
@@ -29,6 +29,7 @@ export interface Subscription {
newestItemTime?: number
filter?: string
filterLegacy?: string
+ pushNotificationsEnabled: boolean
}
export interface Category {
@@ -110,6 +111,7 @@ export interface FeedModificationRequest {
categoryId?: string
position?: number
filter?: string
+ pushNotificationsEnabled: boolean
}
export interface GetEntriesRequest {
@@ -236,6 +238,7 @@ export interface ServerInfo {
forceRefreshCooldownDuration: number
initialSetupRequired: boolean
minimumPasswordLength: number
+ pushNotificationsEnabled: boolean
}
export interface SharingSettings {
@@ -249,6 +252,16 @@ export interface SharingSettings {
buffer: boolean
}
+export type PushNotificationType = "ntfy" | "gotify" | "pushover"
+
+export interface PushNotificationSettings {
+ type?: PushNotificationType
+ serverUrl?: string
+ userId?: string
+ userSecret?: string
+ topic?: string
+}
+
export interface Settings {
language?: string
readingMode: ReadingMode
@@ -271,6 +284,7 @@ export interface Settings {
disablePullToRefresh: boolean
primaryColor?: string
sharingSettings: SharingSettings
+ pushNotificationSettings: PushNotificationSettings
}
export interface LocalSettings {
@@ -290,6 +304,7 @@ export interface SubscribeRequest {
url: string
title: string
categoryId?: string
+ pushNotificationsEnabled: boolean
}
export interface TagRequest {
diff --git a/commafeed-client/src/app/user/slice.ts b/commafeed-client/src/app/user/slice.ts
index 16911038..e093817e 100644
--- a/commafeed-client/src/app/user/slice.ts
+++ b/commafeed-client/src/app/user/slice.ts
@@ -11,6 +11,7 @@ import {
changeMarkAllAsReadConfirmation,
changeMarkAllAsReadNavigateToUnread,
changeMobileFooter,
+ changeNotificationSettings,
changePrimaryColor,
changeReadingMode,
changeReadingOrder,
@@ -148,6 +149,10 @@ export const userSlice = createSlice({
if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
})
+ builder.addCase(changeNotificationSettings.pending, (state, action) => {
+ if (!state.settings) return
+ state.settings.pushNotificationSettings = action.meta.arg
+ })
builder.addMatcher(
isAnyOf(
@@ -167,7 +172,8 @@ export const userSlice = createSlice({
changeUnreadCountFavicon.fulfilled,
changeDisablePullToRefresh.fulfilled,
changePrimaryColor.fulfilled,
- changeSharingSetting.fulfilled
+ changeSharingSetting.fulfilled,
+ changeNotificationSettings.fulfilled
),
() => {
showNotification({
diff --git a/commafeed-client/src/app/user/thunks.ts b/commafeed-client/src/app/user/thunks.ts
index 0dc030af..2d18e9f4 100644
--- a/commafeed-client/src/app/user/thunks.ts
+++ b/commafeed-client/src/app/user/thunks.ts
@@ -1,7 +1,7 @@
import { createAppAsyncThunk } from "@/app/async-thunk"
import { client } from "@/app/client"
import { reloadEntries } from "@/app/entries/thunks"
-import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "@/app/types"
+import type { IconDisplayMode, PushNotificationSettings, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "@/app/types"
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
@@ -157,3 +157,15 @@ export const changeSharingSetting = createAppAsyncThunk(
})
}
)
+
+export const changeNotificationSettings = createAppAsyncThunk(
+ "settings/notificationSettings",
+ (pushNotificationSettings: PushNotificationSettings, thunkApi) => {
+ const { settings } = thunkApi.getState().user
+ if (!settings) return
+ client.user.saveSettings({
+ ...settings,
+ pushNotificationSettings,
+ })
+ }
+)
diff --git a/commafeed-client/src/components/ReceivePushNotificationsChechbox.tsx b/commafeed-client/src/components/ReceivePushNotificationsChechbox.tsx
new file mode 100644
index 00000000..62908d83
--- /dev/null
+++ b/commafeed-client/src/components/ReceivePushNotificationsChechbox.tsx
@@ -0,0 +1,19 @@
+import { Trans } from "@lingui/react/macro"
+import { Checkbox, type CheckboxProps } from "@mantine/core"
+import type { ReactNode } from "react"
+import { useAppSelector } from "@/app/store"
+
+export const ReceivePushNotificationsChechbox = (props: CheckboxProps) => {
+ const pushNotificationsEnabled = useAppSelector(state => state.server.serverInfos?.pushNotificationsEnabled)
+ const pushNotificationsConfigured = useAppSelector(state => !!state.user.settings?.pushNotificationSettings.type)
+
+ const disabled = !pushNotificationsEnabled || !pushNotificationsConfigured
+ let description: ReactNode = ""
+ if (!pushNotificationsEnabled) {
+ description = Push notifications are not enabled on this CommaFeed instance.
+ } else if (!pushNotificationsConfigured) {
+ description = Push notifications are not configured in your user settings.
+ }
+
+ return Receive push notifications} disabled={disabled} description={description} {...props} />
+}
diff --git a/commafeed-client/src/components/content/add/Subscribe.tsx b/commafeed-client/src/components/content/add/Subscribe.tsx
index 973fa28c..86c87d41 100644
--- a/commafeed-client/src/components/content/add/Subscribe.tsx
+++ b/commafeed-client/src/components/content/add/Subscribe.tsx
@@ -11,6 +11,7 @@ import { useAppDispatch } from "@/app/store"
import { reloadTree } from "@/app/tree/thunks"
import type { FeedInfoRequest, SubscribeRequest } from "@/app/types"
import { Alert } from "@/components/Alert"
+import { ReceivePushNotificationsChechbox } from "@/components/ReceivePushNotificationsChechbox"
import { CategorySelect } from "./CategorySelect"
export function Subscribe() {
@@ -28,6 +29,7 @@ export function Subscribe() {
url: "",
title: "",
categoryId: Constants.categories.all.id,
+ pushNotificationsEnabled: false,
},
})
@@ -103,6 +105,9 @@ export function Subscribe() {
Feed URL} {...step1Form.getInputProps("url")} disabled />
Feed name} {...step1Form.getInputProps("title")} required autoFocus />
Category} {...step1Form.getInputProps("categoryId")} clearable />
+
diff --git a/commafeed-client/src/components/settings/PushNotificationSettings.tsx b/commafeed-client/src/components/settings/PushNotificationSettings.tsx
new file mode 100644
index 00000000..76ce7570
--- /dev/null
+++ b/commafeed-client/src/components/settings/PushNotificationSettings.tsx
@@ -0,0 +1,93 @@
+import { useLingui } from "@lingui/react"
+import { Trans } from "@lingui/react/macro"
+import { Button, Group, Select, Stack, TextInput } from "@mantine/core"
+import { useForm } from "@mantine/form"
+import { useEffect } from "react"
+import { TbDeviceFloppy } from "react-icons/tb"
+import { redirectToSelectedSource } from "@/app/redirect/thunks"
+import { useAppDispatch, useAppSelector } from "@/app/store"
+import type { PushNotificationSettings as PushNotificationSettingsModel } from "@/app/types"
+import { changeNotificationSettings } from "@/app/user/thunks"
+
+export function PushNotificationSettings() {
+ const notificationSettings = useAppSelector(state => state.user.settings?.pushNotificationSettings)
+ const pushNotificationsEnabled = useAppSelector(state => state.server.serverInfos?.pushNotificationsEnabled)
+ const { _ } = useLingui()
+ const dispatch = useAppDispatch()
+
+ const form = useForm()
+ useEffect(() => {
+ if (notificationSettings) form.initialize(notificationSettings)
+ }, [form.initialize, notificationSettings])
+
+ const handleSubmit = (values: PushNotificationSettingsModel) => {
+ dispatch(changeNotificationSettings(values))
+ }
+
+ const typeInputProps = form.getInputProps("type")
+
+ if (!pushNotificationsEnabled) {
+ return Push notifications are not enabled on this CommaFeed instance.
+ }
+ return (
+
+ )
+}
diff --git a/commafeed-client/src/locales/ar/messages.po b/commafeed-client/src/locales/ar/messages.po
index f7789af4..91c29d69 100644
--- a/commafeed-client/src/locales/ar/messages.po
+++ b/commafeed-client/src/locales/ar/messages.po
@@ -34,6 +34,10 @@ msgstr "<0> هل تحتاج إلى حساب؟ 0> <1> اشترك! 1>"
msgid "About"
msgstr "حول"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "الإجراءات"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "مفتاح API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "هل أنت متأكد أنك تريد حذف الفئة <0> {categoryName} 0>؟"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "الملف الشخصي"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "استعادة كلمة السر"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "ضع التركيز على الإدخال التالي دون فتحه"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"
@@ -1073,6 +1117,10 @@ msgstr "إلغاء النجم"
msgid "Unsubscribe"
msgstr "إلغاء الاشتراك"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "اسم المستخدم"
diff --git a/commafeed-client/src/locales/ca/messages.po b/commafeed-client/src/locales/ca/messages.po
index 3fb0525b..3452ee1a 100644
--- a/commafeed-client/src/locales/ca/messages.po
+++ b/commafeed-client/src/locales/ca/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Necessites un compte?0><1>Registreu-vos!1>"
msgid "About"
msgstr "Sobre"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Accions"
@@ -94,6 +98,14 @@ msgstr "Anunci"
msgid "API key"
msgstr "Clau API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Esteu segur que voleu suprimir la categoria <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr "Color primari"
msgid "Profile"
msgstr "Perfil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Recuperar la contrasenya"
@@ -841,6 +875,7 @@ msgstr "Clic dret"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr "Selecciona el següent canal/categoria no llegit"
msgid "Select previous unread feed/category"
msgstr "Selecciona el canal/categoria anterior sense llegir"
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Posa el focus a la següent entrada sense obrir-la"
@@ -1050,6 +1090,10 @@ msgstr "Canvia la barra lateral"
msgid "Toggle starred status of current entry"
msgstr "Commuta l'estat destacat de l'entrada actual"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Desestrellar"
msgid "Unsubscribe"
msgstr "Donar-se de baixa"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nom d'usuari"
diff --git a/commafeed-client/src/locales/cs/messages.po b/commafeed-client/src/locales/cs/messages.po
index 6965fb35..e4834036 100644
--- a/commafeed-client/src/locales/cs/messages.po
+++ b/commafeed-client/src/locales/cs/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Potřebujete účet?0><1>Zaregistrujte se!1>"
msgid "About"
msgstr "Asi"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Akce"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "Klíč API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Opravdu chcete smazat kategorii <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Obnovte heslo"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Zaměřte se na další položku, aniž byste ji otevřeli"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Odstranit hvězdu"
msgid "Unsubscribe"
msgstr "Odhlásit odběr"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Uživatelské jméno"
diff --git a/commafeed-client/src/locales/cy/messages.po b/commafeed-client/src/locales/cy/messages.po
index 99bb4b3d..44233875 100644
--- a/commafeed-client/src/locales/cy/messages.po
+++ b/commafeed-client/src/locales/cy/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Angen cyfrif?0><1>Ymunwch!1>"
msgid "About"
msgstr "Ynghylch"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Camau gweithredu"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "Allwedd API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Ydych chi'n siŵr eich bod am ddileu categori <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Proffil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Adfer cyfrinair"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Gosodwch ffocws ar y cofnod nesaf heb ei agor"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "dad-seren"
msgid "Unsubscribe"
msgstr "Dad-danysgrifio"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Enw defnyddiwr"
diff --git a/commafeed-client/src/locales/da/messages.po b/commafeed-client/src/locales/da/messages.po
index a138365f..7c89af9f 100644
--- a/commafeed-client/src/locales/da/messages.po
+++ b/commafeed-client/src/locales/da/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Har du brug for en konto?0><1>Tilmeld dig!1>"
msgid "About"
msgstr "Omkring"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Handlinger"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API-nøgle"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Er du sikker på, at du vil slette kategori <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Gendan adgangskode"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Sæt fokus på næste post uden at åbne den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe"
msgstr "Afmeld"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Brugernavn"
diff --git a/commafeed-client/src/locales/de/messages.po b/commafeed-client/src/locales/de/messages.po
index c810eb96..6bccc982 100644
--- a/commafeed-client/src/locales/de/messages.po
+++ b/commafeed-client/src/locales/de/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Benötigen Sie ein Konto?0><1>Hier geht's zur Registrierung!1>"
msgid "About"
msgstr "Über"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Aktionen"
@@ -94,6 +98,14 @@ msgstr "Ankündigung"
msgid "API key"
msgstr "API-Schlüssel"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Sind Sie sicher, dass Sie die Kategorie <0>{categoryName}0> löschen möchten?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Kennwort wiederherstellen"
@@ -841,6 +875,7 @@ msgstr "Rechtsklick"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Fokus auf den nächsten Eintrag setzen, ohne ihn zu öffnen"
@@ -1050,6 +1090,10 @@ msgstr "Sidebar an- und ausschalten"
msgid "Toggle starred status of current entry"
msgstr "Markierungsstatus des aktuellen Eintrags ändern"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Stern entfernen"
msgid "Unsubscribe"
msgstr "Abbestellen"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Benutzername"
diff --git a/commafeed-client/src/locales/en/messages.po b/commafeed-client/src/locales/en/messages.po
index fd7d9330..f8bff156 100644
--- a/commafeed-client/src/locales/en/messages.po
+++ b/commafeed-client/src/locales/en/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Need an account?0><1>Sign up!1>"
msgid "About"
msgstr "About"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr "Access token"
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Actions"
@@ -94,6 +98,14 @@ msgstr "Announcement"
msgid "API key"
msgstr "API key"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr "API token"
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr "App token"
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Are you sure you want to delete category <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr "Build a filter expression to indicate what you want to read. Entries tha
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr "Primary color"
msgid "Profile"
msgstr "Profile"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr "Push notification service"
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr "Push notifications"
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr "Push notifications are not configured in your user settings."
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr "Push notifications are not enabled on this CommaFeed instance."
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr "Receive push notifications"
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Recover password"
@@ -841,6 +875,7 @@ msgstr "Right click"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr "Select next unread feed/category"
msgid "Select previous unread feed/category"
msgstr "Select previous unread feed/category"
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr "Server URL"
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Set focus on next entry without opening it"
@@ -1050,6 +1090,10 @@ msgstr "Toggle sidebar"
msgid "Toggle starred status of current entry"
msgstr "Toggle starred status of current entry"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr "Topic"
+
#: 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"
@@ -1073,6 +1117,10 @@ msgstr "Unstar"
msgid "Unsubscribe"
msgstr "Unsubscribe"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr "User key"
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "User name"
diff --git a/commafeed-client/src/locales/es/messages.po b/commafeed-client/src/locales/es/messages.po
index 6f4c2534..1fb82fbe 100644
--- a/commafeed-client/src/locales/es/messages.po
+++ b/commafeed-client/src/locales/es/messages.po
@@ -35,6 +35,10 @@ msgstr "<0>¿Necesitas una cuenta?0><1>¡Regístrate!1>"
msgid "About"
msgstr "Acerca de"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Acciones"
@@ -95,6 +99,14 @@ msgstr "Anuncio"
msgid "API key"
msgstr "Clave API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "¿Estás seguro de que deseas eliminar la categoría <0>{categoryName}0>?"
@@ -159,6 +171,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -808,6 +821,27 @@ msgstr ""
msgid "Profile"
msgstr "Perfil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Recuperar contraseña"
@@ -842,6 +876,7 @@ msgstr "Clic derecho"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -874,6 +909,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Establecer el foco en la siguiente entrada sin abrirla"
@@ -1051,6 +1091,10 @@ msgstr "Alternar barra lateral"
msgid "Toggle starred status of current entry"
msgstr "Alternar estado destacado de la entrada actual"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prueba CommaFeed con la cuenta de demostración: demo/demo"
@@ -1074,6 +1118,10 @@ msgstr "Desmarcar"
msgid "Unsubscribe"
msgstr "Cancelar suscripción"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nombre de usuario"
diff --git a/commafeed-client/src/locales/fa/messages.po b/commafeed-client/src/locales/fa/messages.po
index ccda3c16..49972509 100644
--- a/commafeed-client/src/locales/fa/messages.po
+++ b/commafeed-client/src/locales/fa/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>به یک حساب نیاز دارید؟0><1>ثبت نام کنید
msgid "About"
msgstr "در مورد"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "اعمال"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "کلید API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "آیا مطمئن هستید که می خواهید دسته <0>{categoryName}0> را حذف کنید؟"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "نمایه"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "بازیابی رمز عبور"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "فوکوس را روی ورودی بعدی بدون باز کردن آن تنظیم کنید"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe"
msgstr "لغو اشتراک"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "نام کاربری"
diff --git a/commafeed-client/src/locales/fi/messages.po b/commafeed-client/src/locales/fi/messages.po
index 6e0726ca..8c5f7b49 100644
--- a/commafeed-client/src/locales/fi/messages.po
+++ b/commafeed-client/src/locales/fi/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Tarvitsetko tilin?0><1>Rekisteröidy!1>"
msgid "About"
msgstr "Noin"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Toimet"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API-avain"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Haluatko varmasti poistaa luokan <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profiili"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Palauta salasana"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Keskitä seuraavaan merkintään avaamatta sitä"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Poista tähti"
msgid "Unsubscribe"
msgstr "Peruuta tilaus"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Käyttäjänimi"
diff --git a/commafeed-client/src/locales/fr/messages.po b/commafeed-client/src/locales/fr/messages.po
index 6c8bc0dc..1a702eba 100644
--- a/commafeed-client/src/locales/fr/messages.po
+++ b/commafeed-client/src/locales/fr/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Besoin d'un compte ?0><1>Enregistrez-vous !1>"
msgid "About"
msgstr "À propos"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Actions"
@@ -94,6 +98,14 @@ msgstr "Annonces"
msgid "API key"
msgstr "Clé API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Êtes-vous sûr de vouloir supprimer la catégorie <0>{categoryName}0> ?"
@@ -158,6 +170,7 @@ msgstr "Créez une expression régulière pour filtrer ce que vous voulez lire.
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr "Couleur d'ambiance"
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Récupérer le mot de passe"
@@ -841,6 +875,7 @@ msgstr "Clic droit"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr "Sélectionner l'article non lu suivant/la catégorie non lue suivante"
msgid "Select previous unread feed/category"
msgstr "Sélectionner l'article non lu précédent/la catégorie non lue précédente"
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Sélectionner l'article suivant sans l'ouvrir"
@@ -1050,6 +1090,10 @@ msgstr "Montrer/cacher la barre latérale"
msgid "Toggle starred status of current entry"
msgstr "Montrer/cacher le statut favori de l'entrée"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Retirer des favoris"
msgid "Unsubscribe"
msgstr "Se désabonner"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nom"
diff --git a/commafeed-client/src/locales/gl/messages.po b/commafeed-client/src/locales/gl/messages.po
index c665f507..e720227b 100644
--- a/commafeed-client/src/locales/gl/messages.po
+++ b/commafeed-client/src/locales/gl/messages.po
@@ -35,6 +35,10 @@ msgstr "<0>Necesitas unha conta?0><1>Crea unha!1>"
msgid "About"
msgstr "Sobre"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Accións"
@@ -95,6 +99,14 @@ msgstr "Anuncio"
msgid "API key"
msgstr "Clave API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Tes certeza de querer eliminar a categoría <0>{categoryName}0>?"
@@ -159,6 +171,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -808,6 +821,27 @@ msgstr "Cor destacada"
msgid "Profile"
msgstr "Perfil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Recuperar o contrasinal"
@@ -842,6 +876,7 @@ msgstr "Botón dereito"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -874,6 +909,11 @@ msgstr "Seleccionar a seguinte canle/categoría sen ler"
msgid "Select previous unread feed/category"
msgstr "Seleccionar a canle/categoría anterior sen ler"
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Establece o foco no seguinte artigo sen abrilo"
@@ -1051,6 +1091,10 @@ msgstr "Activación do panel lateral"
msgid "Toggle starred status of current entry"
msgstr "Activación do marcado con estrela do artigo actual"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1074,6 +1118,10 @@ msgstr "Retirar estrela"
msgid "Unsubscribe"
msgstr "Cancelar a subscrición"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Identificador"
diff --git a/commafeed-client/src/locales/hu/messages.po b/commafeed-client/src/locales/hu/messages.po
index b79a5398..33583f63 100644
--- a/commafeed-client/src/locales/hu/messages.po
+++ b/commafeed-client/src/locales/hu/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Fiókra van szüksége?0><1>Regisztráljon!1>"
msgid "About"
msgstr "Kb"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Műveletek"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API kulcs"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Biztosan törölni szeretné a(z) <0>{categoryName}0> kategóriát?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Jelszó helyreállítása"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Állítsa a fókuszt a következő bejegyzésre anélkül, hogy megnyitná azt"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe"
msgstr "Leiratkozás"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Felhasználónév"
diff --git a/commafeed-client/src/locales/id/messages.po b/commafeed-client/src/locales/id/messages.po
index f4b4be18..ea8781ae 100644
--- a/commafeed-client/src/locales/id/messages.po
+++ b/commafeed-client/src/locales/id/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Butuh akun?0><1>Daftar!1>"
msgid "About"
msgstr "Tentang"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Tindakan"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "kunci API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Anda yakin ingin menghapus kategori <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Pulihkan kata sandi"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Tetapkan fokus pada entri berikutnya tanpa membukanya"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Hapus bintang"
msgid "Unsubscribe"
msgstr "Berhenti berlangganan"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nama pengguna"
diff --git a/commafeed-client/src/locales/it/messages.po b/commafeed-client/src/locales/it/messages.po
index 36777286..d7fd100f 100644
--- a/commafeed-client/src/locales/it/messages.po
+++ b/commafeed-client/src/locales/it/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Hai bisogno di un account?0><1>Registrati!1>"
msgid "About"
msgstr "Circa"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Azioni"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "Chiave API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Sei sicuro di voler eliminare la categoria <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profilo"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Recupera password"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Imposta il focus sulla voce successiva senza aprirla"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Elimina le stelle"
msgid "Unsubscribe"
msgstr "Annulla iscrizione"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nome utente"
diff --git a/commafeed-client/src/locales/ja/messages.po b/commafeed-client/src/locales/ja/messages.po
index 063277f9..94d5a2e2 100644
--- a/commafeed-client/src/locales/ja/messages.po
+++ b/commafeed-client/src/locales/ja/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>アカウントが必要ですか?0><1>サインアップ!1>"
msgid "About"
msgstr "About"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "アクション"
@@ -94,6 +98,14 @@ msgstr "お知らせ"
msgid "API key"
msgstr "APIキー"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "カテゴリ <0>{categoryName}0> を削除してもよろしいですか?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "プロフィール"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "パスワードの回復"
@@ -841,6 +875,7 @@ msgstr "右クリック"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "次のエントリーを開かずにフォーカスする"
@@ -1050,6 +1090,10 @@ msgstr "サイドバーを切り替える"
msgid "Toggle starred status of current entry"
msgstr "現在のエントリーのスターステータスを切り替える"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "デモアカウントでCommaFeedを試す: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "スターを外す"
msgid "Unsubscribe"
msgstr "退会"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "ユーザー名"
diff --git a/commafeed-client/src/locales/ko/messages.po b/commafeed-client/src/locales/ko/messages.po
index 33bb3c57..00521fbb 100644
--- a/commafeed-client/src/locales/ko/messages.po
+++ b/commafeed-client/src/locales/ko/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>계정이 필요하십니까?0><1>가입하세요!1>"
msgid "About"
msgstr "정보"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "액션"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API 키"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "<0>{categoryName}0> 카테고리를 삭제하시겠습니까?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "프로필"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "비밀번호 복구"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "열지 않고 다음 항목에 포커스 설정"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "별표 제거"
msgid "Unsubscribe"
msgstr "구독 취소"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "사용자 이름"
diff --git a/commafeed-client/src/locales/ms/messages.po b/commafeed-client/src/locales/ms/messages.po
index d15139ab..3cdfc61c 100644
--- a/commafeed-client/src/locales/ms/messages.po
+++ b/commafeed-client/src/locales/ms/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Perlukan akaun?0><1>Daftar!1>"
msgid "About"
msgstr "Mengenai"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Tindakan"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "Kunci API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Adakah anda pasti mahu memadamkan kategori <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Pulihkan kata laluan"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Tetapkan fokus pada entri seterusnya tanpa membukanya"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Nyahbintang"
msgid "Unsubscribe"
msgstr "Nyahlanggan"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nama pengguna"
diff --git a/commafeed-client/src/locales/nb/messages.po b/commafeed-client/src/locales/nb/messages.po
index 96193c1c..f98faf3c 100644
--- a/commafeed-client/src/locales/nb/messages.po
+++ b/commafeed-client/src/locales/nb/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Trenger du en konto?0><1>Registrer deg!1>"
msgid "About"
msgstr "Omtrent"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Handlinger"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API-nøkkel"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Er du sikker på at du vil slette kategori <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Gjenopprett passord"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Sett fokus på neste oppføring uten å åpne den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Fjern stjerne"
msgid "Unsubscribe"
msgstr "Avslutt abonnementet"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Brukernavn"
diff --git a/commafeed-client/src/locales/nl/messages.po b/commafeed-client/src/locales/nl/messages.po
index 9efc4244..3c119d56 100644
--- a/commafeed-client/src/locales/nl/messages.po
+++ b/commafeed-client/src/locales/nl/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Een account nodig?0><1>Meld je aan!1>"
msgid "About"
msgstr "Over"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Acties"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API-sleutel"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Weet je zeker dat je categorie <0>{categoryName}0> wilt verwijderen?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profiel"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "wachtwoord herstellen"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Stel de focus in op het volgende item zonder het te openen"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Sterren uit"
msgid "Unsubscribe"
msgstr "Afmelden"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Gebruikersnaam"
diff --git a/commafeed-client/src/locales/nn/messages.po b/commafeed-client/src/locales/nn/messages.po
index 170b778c..b9bab60e 100644
--- a/commafeed-client/src/locales/nn/messages.po
+++ b/commafeed-client/src/locales/nn/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Trenger du en konto?0><1>Registrer deg!1>"
msgid "About"
msgstr "Omtrent"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Handlinger"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API-nøkkel"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Er du sikker på at du vil slette kategori <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Gjenopprett passord"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Sett fokus på neste oppføring uten å åpne den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Fjern stjerne"
msgid "Unsubscribe"
msgstr "Avslutt abonnementet"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Brukernavn"
diff --git a/commafeed-client/src/locales/pl/messages.po b/commafeed-client/src/locales/pl/messages.po
index 75e0e7ca..54a3e638 100644
--- a/commafeed-client/src/locales/pl/messages.po
+++ b/commafeed-client/src/locales/pl/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Potrzebujesz konta?0><1>Zarejestruj się!1>"
msgid "About"
msgstr "O"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Akcje"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "klucz API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Czy na pewno chcesz usunąć kategorię <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Odzyskaj hasło"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Ustaw fokus na następnym wpisie bez otwierania go"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe"
msgstr "Anuluj subskrypcję"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nazwa użytkownika"
diff --git a/commafeed-client/src/locales/pt/messages.po b/commafeed-client/src/locales/pt/messages.po
index 80aada39..b8aa7402 100644
--- a/commafeed-client/src/locales/pt/messages.po
+++ b/commafeed-client/src/locales/pt/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Precisa de uma conta?0><1>Inscreva-se!1>"
msgid "About"
msgstr "Sobre"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Ações"
@@ -94,6 +98,14 @@ msgstr "Aviso"
msgid "API key"
msgstr "chave de API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Tem certeza de que deseja excluir a categoria <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr "Cor primária"
msgid "Profile"
msgstr "Perfil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Recuperar senha"
@@ -841,6 +875,7 @@ msgstr "Clique com o botão direito"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr "Selecionar próximo feed/categoria não lido"
msgid "Select previous unread feed/category"
msgstr "Selecionar feed/categoria não lido anterior"
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Definir o foco na próxima entrada sem abri-la"
@@ -1050,6 +1090,10 @@ msgstr "Alternar barra lateral"
msgid "Toggle starred status of current entry"
msgstr "Alternar estrela da entrada atual"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Desestrelar"
msgid "Unsubscribe"
msgstr "Cancelar inscrição"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Nome de usuário"
diff --git a/commafeed-client/src/locales/ru/messages.po b/commafeed-client/src/locales/ru/messages.po
index 45c798a7..c90847cf 100644
--- a/commafeed-client/src/locales/ru/messages.po
+++ b/commafeed-client/src/locales/ru/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Нужен аккаунт?0><1>Зарегистрируйтесь!<
msgid "About"
msgstr "О CommaFeed"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Действия"
@@ -94,6 +98,14 @@ msgstr "Объявление"
msgid "API key"
msgstr "Ключ API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Вы уверены, что хотите удалить категорию <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Профиль"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Восстановить пароль"
@@ -841,6 +875,7 @@ msgstr "Правый клик"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Установить фокус на следующую запись, не открывая ее."
@@ -1050,6 +1090,10 @@ msgstr "Переключить боковую панель"
msgid "Toggle starred status of current entry"
msgstr "Переключение статуса избранное для текущей записи"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Попробуйте CommaFeed на демо аккаунте: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Удалить из избранного"
msgid "Unsubscribe"
msgstr "Отписаться"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Имя пользователя"
diff --git a/commafeed-client/src/locales/sk/messages.po b/commafeed-client/src/locales/sk/messages.po
index 6db80d01..08b66310 100644
--- a/commafeed-client/src/locales/sk/messages.po
+++ b/commafeed-client/src/locales/sk/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Potrebujete účet?0><1>Zaregistrujte sa!1>"
msgid "About"
msgstr "Asi"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Akcie"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "Kľúč API"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Naozaj chcete odstrániť kategóriu <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Obnoviť heslo"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Nastavte zameranie na ďalší záznam bez toho, aby ste ho otvorili"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Odobrať hviezdičku"
msgid "Unsubscribe"
msgstr "Zrušte odber"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Meno používateľa"
diff --git a/commafeed-client/src/locales/sv/messages.po b/commafeed-client/src/locales/sv/messages.po
index e33bb2de..74f9cf17 100644
--- a/commafeed-client/src/locales/sv/messages.po
+++ b/commafeed-client/src/locales/sv/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Behöver du ett konto?0><1>Registrera dig!1>"
msgid "About"
msgstr "Ungefär"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Handlingar"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key"
msgstr "API-nyckel"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "Är du säker på att du vill ta bort kategori <0>{categoryName}0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Återställ lösenord"
@@ -841,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Sätt fokus på nästa post utan att öppna den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed med demokontot: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe"
msgstr "Avregistrera"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Användarnamn"
diff --git a/commafeed-client/src/locales/tr/messages.po b/commafeed-client/src/locales/tr/messages.po
index 6fc45639..1f3e8b0d 100644
--- a/commafeed-client/src/locales/tr/messages.po
+++ b/commafeed-client/src/locales/tr/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>Bir hesaba mı ihtiyacınız var?0><1>Kaydolun!1>"
msgid "About"
msgstr "Hakkında"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "Eylemler"
@@ -94,6 +98,14 @@ msgstr "Duyuru"
msgid "API key"
msgstr "API anahtarı"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "<0>{categoryName}0> kategorisini silmek istediğinizden emin misiniz?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile"
msgstr "Profil"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "Şifreyi kurtar"
@@ -841,6 +875,7 @@ msgstr "Sağ tık"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "Odağı açmadan sonraki girişe ayarlayın"
@@ -1050,6 +1090,10 @@ msgstr "Kenar çubuğunu göster/gizle"
msgid "Toggle starred status of current entry"
msgstr ""
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+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"
@@ -1073,6 +1117,10 @@ msgstr "Yıldızı kaldır"
msgid "Unsubscribe"
msgstr "Aboneliği iptal et"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "Kullanıcı adı"
diff --git a/commafeed-client/src/locales/zh/messages.po b/commafeed-client/src/locales/zh/messages.po
index baad539b..7dc6db26 100644
--- a/commafeed-client/src/locales/zh/messages.po
+++ b/commafeed-client/src/locales/zh/messages.po
@@ -34,6 +34,10 @@ msgstr "<0>需要一个帐户?0><1>注册!1>"
msgid "About"
msgstr "关于"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Access token"
+msgstr ""
+
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
msgstr "操作"
@@ -94,6 +98,14 @@ msgstr "公告"
msgid "API key"
msgstr "API 密钥"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "API token"
+msgstr ""
+
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "App token"
+msgstr ""
+
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}0>?"
msgstr "您确定要删除类别 <0>{categoryName}0> 吗?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -807,6 +820,27 @@ msgstr "主颜色"
msgid "Profile"
msgstr "配置文件"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notification service"
+msgstr ""
+
+#: src/pages/app/SettingsPage.tsx
+msgid "Push notifications"
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Push notifications are not configured in your user settings."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Push notifications are not enabled on this CommaFeed instance."
+msgstr ""
+
+#: src/components/ReceivePushNotificationsChechbox.tsx
+msgid "Receive push notifications"
+msgstr ""
+
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password"
msgstr "找回密码"
@@ -841,6 +875,7 @@ msgstr "右键单击"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Save"
@@ -873,6 +908,11 @@ msgstr "选择下一个未读信息流/类别"
msgid "Select previous unread feed/category"
msgstr "选择上一个未读信息流/类别"
+#: src/components/settings/PushNotificationSettings.tsx
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Server URL"
+msgstr ""
+
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it"
msgstr "将焦点放在下一个条目而不打开它"
@@ -1050,6 +1090,10 @@ msgstr "切换侧边栏"
msgid "Toggle starred status of current entry"
msgstr "切换当前条目的星标状态"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "Topic"
+msgstr ""
+
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "使用演示帐户试用 CommaFeed:demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "取消星标"
msgid "Unsubscribe"
msgstr "取消订阅"
+#: src/components/settings/PushNotificationSettings.tsx
+msgid "User key"
+msgstr ""
+
#: src/components/settings/ProfileSettings.tsx
msgid "User name"
msgstr "用户名"
diff --git a/commafeed-client/src/pages/admin/MetricsPage.tsx b/commafeed-client/src/pages/admin/MetricsPage.tsx
index c19c252c..5bb97ba8 100644
--- a/commafeed-client/src/pages/admin/MetricsPage.tsx
+++ b/commafeed-client/src/pages/admin/MetricsPage.tsx
@@ -19,10 +19,8 @@ const shownGauges: Record = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Feed Refresh Engine queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Refresh Engine active HTTP workers",
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Refresh Engine active database update workers",
- "com.commafeed.backend.HttpGetter.pool.max": "HttpGetter max pool size",
- "com.commafeed.backend.HttpGetter.pool.size": "HttpGetter current pool size",
- "com.commafeed.backend.HttpGetter.pool.leased": "HttpGetter active connections",
- "com.commafeed.backend.HttpGetter.pool.pending": "HttpGetter waiting for a connection",
+ "com.commafeed.backend.feed.FeedRefreshEngine.notifier.active": "Feed Refresh Engine active push notifications workers",
+ "com.commafeed.backend.feed.FeedRefreshEngine.notifier.queue": "Feed Refresh Engine queued push notifications workers",
"com.commafeed.backend.HttpGetter.cache.size": "HttpGetter cached entries",
"com.commafeed.backend.HttpGetter.cache.memoryUsage": "HttpGetter cache memory usage",
"com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users",
diff --git a/commafeed-client/src/pages/app/FeedDetailsPage.tsx b/commafeed-client/src/pages/app/FeedDetailsPage.tsx
index a6575942..d7cc1727 100644
--- a/commafeed-client/src/pages/app/FeedDetailsPage.tsx
+++ b/commafeed-client/src/pages/app/FeedDetailsPage.tsx
@@ -30,6 +30,7 @@ import { Alert } from "@/components/Alert"
import { CategorySelect } from "@/components/content/add/CategorySelect"
import { FilteringExpressionEditor } from "@/components/content/edit/FilteringExpressionEditor"
import { Loader } from "@/components/Loader"
+import { ReceivePushNotificationsChechbox } from "@/components/ReceivePushNotificationsChechbox"
import { RelativeDate } from "@/components/RelativeDate"
export function FeedDetailsPage() {
@@ -142,6 +143,7 @@ export function FeedDetailsPage() {
Name} {...form.getInputProps("name")} required />
Category} {...form.getInputProps("categoryId")} clearable />
Position} {...form.getInputProps("position")} required min={0} />
+
Filtering expression}
description={
diff --git a/commafeed-client/src/pages/app/SettingsPage.tsx b/commafeed-client/src/pages/app/SettingsPage.tsx
index 1177e40b..c5bc0fe2 100644
--- a/commafeed-client/src/pages/app/SettingsPage.tsx
+++ b/commafeed-client/src/pages/app/SettingsPage.tsx
@@ -1,9 +1,10 @@
import { Trans } from "@lingui/react/macro"
import { Container, Tabs } from "@mantine/core"
-import { TbCode, TbPhoto, TbUser } from "react-icons/tb"
+import { TbBell, TbCode, TbPhoto, TbUser } from "react-icons/tb"
import { CustomCodeSettings } from "@/components/settings/CustomCodeSettings"
import { DisplaySettings } from "@/components/settings/DisplaySettings"
import { ProfileSettings } from "@/components/settings/ProfileSettings"
+import { PushNotificationSettings } from "@/components/settings/PushNotificationSettings"
export function SettingsPage() {
return (
@@ -13,6 +14,9 @@ export function SettingsPage() {
}>
Display
+ }>
+ Push notifications
+
}>
Custom code
@@ -25,6 +29,10 @@ export function SettingsPage() {
+
+
+
+
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
index d2ca11c8..06336404 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
@@ -68,6 +68,12 @@ public interface CommaFeedConfiguration {
@ConfigDocSection
FeedRefresh feedRefresh();
+ /**
+ * Push notification settings.
+ */
+ @ConfigDocSection
+ PushNotifications pushNotifications();
+
/**
* Database settings.
*/
@@ -138,10 +144,9 @@ public interface CommaFeedConfiguration {
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
* resources.
*
- * You may want to disable this if you subscribe to feeds that are only available on your local network and you trust all users of
- * your CommaFeed instance.
+ * You may want to enable this if you host a public instance of CommaFeed with regisration open.
*/
- @WithDefault("true")
+ @WithDefault("false")
boolean blockLocalAddresses();
/**
@@ -242,6 +247,28 @@ public interface CommaFeedConfiguration {
Duration forceRefreshCooldownDuration();
}
+ interface PushNotifications {
+ /**
+ * Whether to enable push notifications to notify users of new entries in their feeds.
+ */
+ @WithDefault("true")
+ boolean enabled();
+
+ /**
+ * Amount of threads used to send external notifications about new entries.
+ */
+ @Min(1)
+ @WithDefault("5")
+ int threads();
+
+ /**
+ * Maximum amount of notifications that can be queued before new notifications are discarded.
+ */
+ @Min(1)
+ @WithDefault("100")
+ int queueCapacity();
+ }
+
interface FeedRefreshErrorHandling {
/**
* Number of retries before backoff is applied.
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java
new file mode 100644
index 00000000..d0c62977
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java
@@ -0,0 +1,124 @@
+package com.commafeed.backend;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.SequencedMap;
+import java.util.zip.GZIPInputStream;
+
+import jakarta.inject.Singleton;
+
+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.TlsConfig;
+import org.apache.hc.client5.http.entity.DeflateInputStream;
+import org.apache.hc.client5.http.entity.InputStreamFactory;
+import org.apache.hc.client5.http.entity.compress.ContentCoding;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.brotli.dec.BrotliInputStream;
+
+import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.CommaFeedVersion;
+import com.google.common.net.HttpHeaders;
+
+import lombok.RequiredArgsConstructor;
+import nl.altindag.ssl.SSLFactory;
+import nl.altindag.ssl.apache5.util.Apache5SslUtils;
+
+@Singleton
+@RequiredArgsConstructor
+public class HttpClientFactory {
+
+ private final CommaFeedConfiguration config;
+ private final CommaFeedVersion version;
+
+ public CloseableHttpClient newClient(int poolSize) {
+ PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config, poolSize);
+ String userAgent = config.httpClient()
+ .userAgent()
+ .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion()));
+ return newClient(connectionManager, userAgent, config.httpClient().idleConnectionsEvictionInterval());
+ }
+
+ private CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent,
+ Duration idleConnectionsEvictionInterval) {
+ List headers = new ArrayList<>();
+ headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
+ headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
+ headers.add(new BasicHeader(HttpHeaders.CACHE_CONTROL, "no-cache"));
+
+ SequencedMap contentDecoderMap = new LinkedHashMap<>();
+ contentDecoderMap.put(ContentCoding.GZIP.token(), GZIPInputStream::new);
+ contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new);
+ contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new);
+
+ return HttpClientBuilder.create()
+ .useSystemProperties()
+ .disableAutomaticRetries()
+ .disableCookieManagement()
+ .setUserAgent(userAgent)
+ .setDefaultHeaders(headers)
+ .setConnectionManager(connectionManager)
+ .evictExpiredConnections()
+ .evictIdleConnections(TimeValue.of(idleConnectionsEvictionInterval))
+ .setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap))
+ .build();
+ }
+
+ private PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config, int poolSize) {
+ SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
+ DnsResolver dnsResolver = config.httpClient().blockLocalAddresses()
+ ? new BlockLocalAddressesDnsResolver(SystemDefaultDnsResolver.INSTANCE)
+ : SystemDefaultDnsResolver.INSTANCE;
+
+ return PoolingHttpClientConnectionManagerBuilder.create()
+ .setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory))
+ .setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
+ .setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
+ .setTimeToLive(Timeout.of(config.httpClient().connectionTimeToLive()))
+ .build())
+ .setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
+ .setMaxConnPerRoute(poolSize)
+ .setMaxConnTotal(poolSize)
+ .setDnsResolver(dnsResolver)
+ .build();
+
+ }
+
+ private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver {
+ @Override
+ public InetAddress[] resolve(String host) throws UnknownHostException {
+ InetAddress[] addresses = delegate.resolve(host);
+ for (InetAddress addr : addresses) {
+ if (isLocalAddress(addr)) {
+ throw new UnknownHostException("Access to address blocked: " + addr.getHostAddress());
+ }
+ }
+ return addresses;
+ }
+
+ @Override
+ public String resolveCanonicalHostname(String host) throws UnknownHostException {
+ return delegate.resolveCanonicalHostname(host);
+ }
+
+ private boolean isLocalAddress(InetAddress address) {
+ return address.isSiteLocalAddress() || address.isAnyLocalAddress() || address.isLinkLocalAddress()
+ || address.isLoopbackAddress() || address.isMulticastAddress();
+ }
+ }
+
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
index 7c1bce65..67ca9ce9 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
@@ -2,20 +2,12 @@ package com.commafeed.backend;
import java.io.IOException;
import java.io.InputStream;
-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.LinkedHashMap;
-import java.util.List;
import java.util.Optional;
-import java.util.SequencedMap;
import java.util.concurrent.ExecutionException;
-import java.util.stream.Stream;
-import java.util.zip.GZIPInputStream;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.CacheControl;
@@ -24,36 +16,22 @@ import org.apache.commons.lang3.ArrayUtils;
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;
-import org.apache.hc.client5.http.entity.DeflateInputStream;
-import org.apache.hc.client5.http.entity.InputStreamFactory;
-import org.apache.hc.client5.http.entity.compress.ContentCoding;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
-import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
-import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
-import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
-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;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
-import org.apache.hc.core5.http.message.BasicHeader;
-import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
-import org.brotli.dec.BrotliInputStream;
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.HttpClientCache;
-import com.commafeed.CommaFeedVersion;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Iterables;
@@ -66,8 +44,6 @@ import lombok.Getter;
import lombok.Lombok;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import nl.altindag.ssl.SSLFactory;
-import nl.altindag.ssl.apache5.util.Apache5SslUtils;
/**
* Smart HTTP getter: handles gzip, ssl, last modified and etag headers
@@ -82,42 +58,27 @@ public class HttpGetter {
private final CloseableHttpClient client;
private final Cache cache;
- public HttpGetter(CommaFeedConfiguration config, InstantSource instantSource, CommaFeedVersion version, MetricRegistry metrics) {
+ public HttpGetter(CommaFeedConfiguration config, InstantSource instantSource, HttpClientFactory httpClientFactory,
+ MetricRegistry metrics) {
this.config = config;
this.instantSource = instantSource;
-
- PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config);
- String userAgent = config.httpClient()
- .userAgent()
- .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion()));
-
- this.client = newClient(connectionManager, userAgent, config.httpClient().idleConnectionsEvictionInterval());
+ this.client = httpClientFactory.newClient(config.feedRefresh().httpThreads());
this.cache = newCache(config);
- metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "max"), () -> connectionManager.getTotalStats().getMax());
- metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "size"),
- () -> connectionManager.getTotalStats().getAvailable() + connectionManager.getTotalStats().getLeased());
- metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "leased"), () -> connectionManager.getTotalStats().getLeased());
- metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "pending"), () -> connectionManager.getTotalStats().getPending());
metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "size"), () -> cache == null ? 0 : cache.size());
metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "memoryUsage"),
() -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> ArrayUtils.getLength(e.content)).sum());
}
- public HttpResult get(String url)
- throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException {
+ public HttpResult get(String url) throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
return get(HttpRequest.builder(url).build());
}
public HttpResult get(HttpRequest request)
- throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException {
+ throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
URI uri = URI.create(request.getUrl());
ensureHttpScheme(uri.getScheme());
- if (config.httpClient().blockLocalAddresses()) {
- ensurePublicAddress(uri.getHost());
- }
-
final HttpResponse response;
if (cache == null) {
response = invoke(request);
@@ -164,22 +125,6 @@ public class HttpGetter {
}
}
- private void ensurePublicAddress(String host) throws HostNotAllowedException, UnknownHostException {
- if (host == null) {
- throw new HostNotAllowedException(null);
- }
-
- InetAddress[] addresses = DNS_RESOLVER.resolve(host);
- if (Stream.of(addresses).anyMatch(this::isPrivateAddress)) {
- throw new HostNotAllowedException(host);
- }
- }
-
- private boolean isPrivateAddress(InetAddress address) {
- return address.isSiteLocalAddress() || address.isAnyLocalAddress() || address.isLinkLocalAddress() || address.isLoopbackAddress()
- || address.isMulticastAddress();
- }
-
private HttpResponse invoke(HttpRequest request) throws IOException {
log.debug("fetching {}", request.getUrl());
@@ -268,50 +213,6 @@ public class HttpGetter {
}
}
- private PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config) {
- SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
-
- int poolSize = config.feedRefresh().httpThreads();
- return PoolingHttpClientConnectionManagerBuilder.create()
- .setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory))
- .setDefaultConnectionConfig(ConnectionConfig.custom()
- .setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
- .setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
- .setTimeToLive(Timeout.of(config.httpClient().connectionTimeToLive()))
- .build())
- .setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
- .setMaxConnPerRoute(poolSize)
- .setMaxConnTotal(poolSize)
- .setDnsResolver(DNS_RESOLVER)
- .build();
-
- }
-
- private static CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent,
- Duration idleConnectionsEvictionInterval) {
- List headers = new ArrayList<>();
- headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
- headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
- headers.add(new BasicHeader(HttpHeaders.CACHE_CONTROL, "no-cache"));
-
- SequencedMap contentDecoderMap = new LinkedHashMap<>();
- contentDecoderMap.put(ContentCoding.GZIP.token(), GZIPInputStream::new);
- contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new);
- contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new);
-
- return HttpClientBuilder.create()
- .useSystemProperties()
- .disableAutomaticRetries()
- .disableCookieManagement()
- .setUserAgent(userAgent)
- .setDefaultHeaders(headers)
- .setConnectionManager(connectionManager)
- .evictExpiredConnections()
- .evictIdleConnections(TimeValue.of(idleConnectionsEvictionInterval))
- .setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap))
- .build();
- }
-
private static Cache newCache(CommaFeedConfiguration config) {
HttpClientCache cacheConfig = config.httpClient().cache();
if (!cacheConfig.enabled()) {
@@ -333,14 +234,6 @@ public class HttpGetter {
}
}
- public static class HostNotAllowedException extends Exception {
- private static final long serialVersionUID = 1L;
-
- public HostNotAllowedException(String host) {
- super("Host not allowed: " + host);
- }
- }
-
@Getter
public static class NotModifiedException extends Exception {
private static final long serialVersionUID = 1L;
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/Urls.java b/commafeed-server/src/main/java/com/commafeed/backend/Urls.java
index bdf36e1d..7a5e78c7 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/Urls.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/Urls.java
@@ -53,6 +53,10 @@ public class Urls {
}
public static String removeTrailingSlash(String url) {
+ if (url == null) {
+ return null;
+ }
+
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
index 141c0e7d..fa457a61 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
@@ -14,7 +14,6 @@ import org.apache.hc.core5.net.URIBuilder;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter;
-import com.commafeed.backend.HttpGetter.HostNotAllowedException;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.SchemeNotAllowedException;
@@ -98,7 +97,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
}
private byte[] fetchForUser(String googleAuthKey, String userId)
- throws IOException, NotModifiedException, TooManyRequestsException, HostNotAllowedException, SchemeNotAllowedException {
+ throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey)
@@ -108,7 +107,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
}
private byte[] fetchForChannel(String googleAuthKey, String channelId)
- throws IOException, NotModifiedException, TooManyRequestsException, HostNotAllowedException, SchemeNotAllowedException {
+ throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey)
@@ -118,7 +117,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
}
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId)
- throws IOException, NotModifiedException, TooManyRequestsException, HostNotAllowedException, SchemeNotAllowedException {
+ throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
.queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey)
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
index c468d186..bd041cc8 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
@@ -13,7 +13,6 @@ import org.apache.commons.lang3.Strings;
import com.commafeed.backend.Digests;
import com.commafeed.backend.HttpGetter;
-import com.commafeed.backend.HttpGetter.HostNotAllowedException;
import com.commafeed.backend.HttpGetter.HttpRequest;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
@@ -46,7 +45,7 @@ public class FeedFetcher {
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
Instant lastPublishedDate, String lastContentHash) throws FeedParsingException, IOException, NotModifiedException,
- TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException, NoFeedFoundException {
+ TooManyRequestsException, SchemeNotAllowedException, NoFeedFoundException {
log.debug("Fetching feed {}", feedUrl);
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
index 6c3b52c1..42b7f18c 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
@@ -7,6 +7,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -21,6 +22,8 @@ import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed;
+import com.commafeed.backend.model.FeedEntry;
+import com.commafeed.backend.model.FeedSubscription;
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +35,7 @@ public class FeedRefreshEngine {
private final FeedDAO feedDAO;
private final FeedRefreshWorker worker;
private final FeedRefreshUpdater updater;
+ private final FeedUpdateNotifier notifier;
private final CommaFeedConfiguration config;
private final Meter refill;
@@ -42,13 +46,15 @@ public class FeedRefreshEngine {
private final ExecutorService refillExecutor;
private final ThreadPoolExecutor workerExecutor;
private final ThreadPoolExecutor databaseUpdaterExecutor;
+ private final ThreadPoolExecutor notifierExecutor;
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
- CommaFeedConfiguration config, MetricRegistry metrics) {
+ FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO;
this.worker = worker;
this.updater = updater;
+ this.notifier = notifier;
this.config = config;
this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
@@ -59,10 +65,14 @@ public class FeedRefreshEngine {
this.refillExecutor = newDiscardingSingleThreadExecutorService();
this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
+ this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
+ config.pushNotifications().queueCapacity());
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge) queue::size);
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge) workerExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge) databaseUpdaterExecutor::getActiveCount);
+ metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge) notifierExecutor::getActiveCount);
+ metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge) () -> notifierExecutor.getQueue().size());
}
public void start() {
@@ -152,10 +162,19 @@ public class FeedRefreshEngine {
private void processFeedAsync(Feed feed) {
CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
.thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
- .whenComplete((data, ex) -> {
- if (ex != null) {
- log.error("error while processing feed {}", feed.getUrl(), ex);
- }
+ .thenCompose(r -> {
+ List> futures = r.insertedUnreadEntriesBySubscription().entrySet().stream().map(e -> {
+ FeedSubscription sub = e.getKey();
+ List entries = e.getValue();
+
+ notifier.notifyOverWebsocket(sub, entries);
+ return CompletableFuture.runAsync(() -> notifier.sendPushNotifications(sub, entries), notifierExecutor);
+ }).toList();
+ return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
+ })
+ .exceptionally(ex -> {
+ log.error("error while processing feed {}", feed.getUrl(), ex);
+ return null;
});
}
@@ -183,6 +202,7 @@ public class FeedRefreshEngine {
this.refillExecutor.shutdownNow();
this.workerExecutor.shutdownNow();
this.databaseUpdaterExecutor.shutdownNow();
+ this.notifierExecutor.shutdownNow();
}
/**
@@ -194,6 +214,16 @@ public class FeedRefreshEngine {
return pool;
}
+ /**
+ * returns an ExecutorService that discards tasks if the queue is full
+ */
+ private ThreadPoolExecutor newDiscardingExecutorService(int threads, int queueCapacity) {
+ ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<>(queueCapacity));
+ pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
+ return pool;
+ }
+
/**
* returns an ExecutorService that blocks submissions until a thread is available
*/
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
index e7b1c522..e2c0d5ba 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
@@ -1,5 +1,6 @@
package com.commafeed.backend.feed;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
@@ -27,8 +28,6 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
-import com.commafeed.frontend.ws.WebSocketMessageBuilder;
-import com.commafeed.frontend.ws.WebSocketSessions;
import com.google.common.util.concurrent.Striped;
import lombok.extern.slf4j.Slf4j;
@@ -44,7 +43,6 @@ public class FeedRefreshUpdater {
private final FeedService feedService;
private final FeedEntryService feedEntryService;
private final FeedSubscriptionDAO feedSubscriptionDAO;
- private final WebSocketSessions webSocketSessions;
private final Striped locks;
@@ -52,12 +50,11 @@ public class FeedRefreshUpdater {
private final Meter entryInserted;
public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics,
- FeedSubscriptionDAO feedSubscriptionDAO, WebSocketSessions webSocketSessions) {
+ FeedSubscriptionDAO feedSubscriptionDAO) {
this.unitOfWork = unitOfWork;
this.feedService = feedService;
this.feedEntryService = feedEntryService;
this.feedSubscriptionDAO = feedSubscriptionDAO;
- this.webSocketSessions = webSocketSessions;
locks = Striped.lazyWeakLock(100000);
@@ -67,7 +64,7 @@ public class FeedRefreshUpdater {
private AddEntryResult addEntry(final Feed feed, final Entry entry, final List subscriptions) {
boolean processed = false;
- boolean inserted = false;
+ FeedEntry insertedEntry = null;
Set subscriptionsForWhichEntryIsUnread = new HashSet<>();
// lock on feed, make sure we are not updating the same feed twice at
@@ -90,23 +87,21 @@ public class FeedRefreshUpdater {
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) {
processed = true;
- inserted = unitOfWork.call(() -> {
- boolean newEntry = false;
- FeedEntry feedEntry = feedEntryService.find(feed, entry);
- if (feedEntry == null) {
- feedEntry = feedEntryService.create(feed, entry);
- newEntry = true;
+ insertedEntry = unitOfWork.call(() -> {
+ if (feedEntryService.find(feed, entry) != null) {
+ // entry already exists, nothing to do
+ return null;
}
- if (newEntry) {
- entryInserted.mark();
- for (FeedSubscription sub : subscriptions) {
- boolean unread = feedEntryService.applyFilter(sub, feedEntry);
- if (unread) {
- subscriptionsForWhichEntryIsUnread.add(sub);
- }
+
+ FeedEntry feedEntry = feedEntryService.create(feed, entry);
+ entryInserted.mark();
+ for (FeedSubscription sub : subscriptions) {
+ boolean unread = feedEntryService.applyFilter(sub, feedEntry);
+ if (unread) {
+ subscriptionsForWhichEntryIsUnread.add(sub);
}
}
- return newEntry;
+ return feedEntry;
});
} else {
log.error("lock timeout for {} - {}", feed.getUrl(), key1);
@@ -122,13 +117,13 @@ public class FeedRefreshUpdater {
lock2.unlock();
}
}
- return new AddEntryResult(processed, inserted, subscriptionsForWhichEntryIsUnread);
+ return new AddEntryResult(processed, insertedEntry, subscriptionsForWhichEntryIsUnread);
}
- public boolean update(Feed feed, List entries) {
+ public FeedRefreshUpdaterResult update(Feed feed, List entries) {
boolean processed = true;
long inserted = 0;
- Map unreadCountBySubscription = new HashMap<>();
+ Map> insertedUnreadEntriesBySubscription = new HashMap<>();
if (!entries.isEmpty()) {
List subscriptions = null;
@@ -138,8 +133,12 @@ public class FeedRefreshUpdater {
}
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
processed &= addEntryResult.processed;
- inserted += addEntryResult.inserted ? 1 : 0;
- addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum));
+ inserted += addEntryResult.insertedEntry != null ? 1 : 0;
+ addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> {
+ if (addEntryResult.insertedEntry != null) {
+ insertedUnreadEntriesBySubscription.computeIfAbsent(sub, k -> new ArrayList<>()).add(addEntryResult.insertedEntry);
+ }
+ });
}
if (inserted == 0) {
@@ -160,17 +159,13 @@ public class FeedRefreshUpdater {
unitOfWork.run(() -> feedService.update(feed));
- notifyOverWebsocket(unreadCountBySubscription);
-
- return processed;
+ return new FeedRefreshUpdaterResult(insertedUnreadEntriesBySubscription);
}
- private void notifyOverWebsocket(Map unreadCountBySubscription) {
- unreadCountBySubscription.forEach((sub, unreadCount) -> webSocketSessions.sendMessage(sub.getUser(),
- WebSocketMessageBuilder.newFeedEntries(sub, unreadCount)));
+ private record AddEntryResult(boolean processed, FeedEntry insertedEntry, Set subscriptionsForWhichEntryIsUnread) {
}
- private record AddEntryResult(boolean processed, boolean inserted, Set subscriptionsForWhichEntryIsUnread) {
+ public record FeedRefreshUpdaterResult(Map> insertedUnreadEntriesBySubscription) {
}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUpdateNotifier.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUpdateNotifier.java
new file mode 100644
index 00000000..5ffe1f4f
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUpdateNotifier.java
@@ -0,0 +1,50 @@
+package com.commafeed.backend.feed;
+
+import java.util.List;
+
+import jakarta.inject.Singleton;
+
+import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.backend.dao.UnitOfWork;
+import com.commafeed.backend.dao.UserSettingsDAO;
+import com.commafeed.backend.model.FeedEntry;
+import com.commafeed.backend.model.FeedSubscription;
+import com.commafeed.backend.model.UserSettings;
+import com.commafeed.backend.service.PushNotificationService;
+import com.commafeed.frontend.ws.WebSocketMessageBuilder;
+import com.commafeed.frontend.ws.WebSocketSessions;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Singleton
+@RequiredArgsConstructor
+public class FeedUpdateNotifier {
+
+ private final CommaFeedConfiguration config;
+ private final UnitOfWork unitOfWork;
+ private final UserSettingsDAO userSettingsDAO;
+ private final WebSocketSessions webSocketSessions;
+ private final PushNotificationService pushNotificationService;
+
+ public void notifyOverWebsocket(FeedSubscription sub, List entries) {
+ if (!entries.isEmpty()) {
+ webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size()));
+ }
+ }
+
+ public void sendPushNotifications(FeedSubscription sub, List entries) {
+ if (!config.pushNotifications().enabled() || !sub.isPushNotificationsEnabled() || entries.isEmpty()) {
+ return;
+ }
+
+ UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(sub.getUser()));
+ if (settings != null && settings.getPushNotificationType() != null) {
+ for (FeedEntry entry : entries) {
+ pushNotificationService.notify(settings, sub, entry);
+ }
+ }
+ }
+
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java
index 91807cb4..41a0e4dd 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java
@@ -46,4 +46,7 @@ public class FeedSubscription extends AbstractModel {
@Column(name = "filtering_expression_legacy", length = 4096)
private String filterLegacy;
+ @Column(name = "push_notifications_enabled")
+ private boolean pushNotificationsEnabled;
+
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java b/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java
index 485c4941..95b9093a 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java
@@ -77,6 +77,17 @@ public class UserSettings extends AbstractModel {
ON_MOBILE
}
+ public enum PushNotificationType {
+ @JsonProperty("ntfy")
+ NTFY,
+
+ @JsonProperty("gotify")
+ GOTIFY,
+
+ @JsonProperty("pushover")
+ PUSHOVER
+ }
+
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false, unique = true)
private User user;
@@ -133,6 +144,22 @@ public class UserSettings extends AbstractModel {
private boolean unreadCountFavicon;
private boolean disablePullToRefresh;
+ @Enumerated(EnumType.STRING)
+ @Column(name = "push_notification_type", length = 16)
+ private PushNotificationType pushNotificationType;
+
+ @Column(name = "push_notification_server_url", length = 1024)
+ private String pushNotificationServerUrl;
+
+ @Column(name = "push_notification_user_id", length = 512)
+ private String pushNotificationUserId;
+
+ @Column(name = "push_notification_user_secret", length = 512)
+ private String pushNotificationUserSecret;
+
+ @Column(name = "push_notification_topic", length = 256)
+ private String pushNotificationTopic;
+
private boolean email;
private boolean gmail;
private boolean facebook;
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java
index ac575c5b..4729eee3 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java
@@ -77,7 +77,7 @@ public class OPMLImporter {
}
// make sure we continue with the import process even if a feed failed
try {
- feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position);
+ feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position, false);
} catch (Exception e) {
log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java
index fa9c8cc5..7b109594 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java
@@ -49,15 +49,7 @@ public class FeedSubscriptionService {
});
}
- public long subscribe(User user, String url, String title) {
- return subscribe(user, url, title, null, 0);
- }
-
- public long subscribe(User user, String url, String title, FeedCategory parent) {
- return subscribe(user, url, title, parent, 0);
- }
-
- public long subscribe(User user, String url, String title, FeedCategory category, int position) {
+ public long subscribe(User user, String url, String title, FeedCategory category, int position, boolean pushNotificationsEnabled) {
Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser();
if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) {
String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)",
@@ -81,6 +73,7 @@ public class FeedSubscriptionService {
sub.setCategory(category);
sub.setPosition(position);
sub.setTitle(FeedUtils.truncate(title, 128));
+ sub.setPushNotificationsEnabled(pushNotificationsEnabled);
return feedSubscriptionDAO.merge(sub).getId();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java
new file mode 100644
index 00000000..bc5c9318
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java
@@ -0,0 +1,178 @@
+package com.commafeed.backend.service;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.inject.Singleton;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.apache.hc.core5.util.Timeout;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.backend.HttpClientFactory;
+import com.commafeed.backend.Urls;
+import com.commafeed.backend.model.FeedEntry;
+import com.commafeed.backend.model.FeedSubscription;
+import com.commafeed.backend.model.UserSettings;
+
+import io.vertx.core.json.JsonObject;
+import lombok.extern.slf4j.Slf4j;
+
+@Singleton
+@Slf4j
+public class PushNotificationService {
+
+ private final CloseableHttpClient httpClient;
+ private final Meter meter;
+ private final CommaFeedConfiguration config;
+
+ public PushNotificationService(HttpClientFactory httpClientFactory, MetricRegistry metrics, CommaFeedConfiguration config) {
+ this.httpClient = httpClientFactory.newClient(config.pushNotifications().threads());
+ this.meter = metrics.meter(MetricRegistry.name(getClass(), "notify"));
+ this.config = config;
+ }
+
+ public void notify(UserSettings settings, FeedSubscription subscription, FeedEntry entry) {
+ if (!config.pushNotifications().enabled() || settings.getPushNotificationType() == null) {
+ return;
+ }
+
+ log.debug("sending {} push notification for entry {} in feed {}", settings.getPushNotificationType(), entry.getId(),
+ subscription.getFeed().getId());
+ String entryTitle = entry.getContent() != null ? entry.getContent().getTitle() : null;
+ String entryUrl = entry.getUrl();
+ String feedTitle = subscription.getTitle();
+
+ if (StringUtils.isBlank(entryTitle)) {
+ entryTitle = "New entry";
+ }
+
+ try {
+ switch (settings.getPushNotificationType()) {
+ case NTFY -> sendNtfy(settings, feedTitle, entryTitle, entryUrl);
+ case GOTIFY -> sendGotify(settings, feedTitle, entryTitle, entryUrl);
+ case PUSHOVER -> sendPushover(settings, feedTitle, entryTitle, entryUrl);
+ default -> throw new IllegalStateException("unsupported notification type: " + settings.getPushNotificationType());
+ }
+ } catch (IOException e) {
+ throw new PushNotificationException("Failed to send external notification", e);
+ }
+
+ meter.mark();
+ }
+
+ private void sendNtfy(UserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws IOException {
+ String serverUrl = Urls.removeTrailingSlash(settings.getPushNotificationServerUrl());
+ String topic = settings.getPushNotificationTopic();
+
+ if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(topic)) {
+ log.warn("ntfy notification skipped: missing server URL or topic");
+ return;
+ }
+
+ HttpPost request = new HttpPost(serverUrl + "/" + topic);
+ request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build());
+ request.addHeader("Title", feedTitle);
+ request.setEntity(new StringEntity(entryTitle, StandardCharsets.UTF_8));
+
+ if (StringUtils.isNotBlank(entryUrl)) {
+ request.addHeader("Click", entryUrl);
+ }
+
+ if (StringUtils.isNotBlank(settings.getPushNotificationUserSecret())) {
+ request.addHeader("Authorization", "Bearer " + settings.getPushNotificationUserSecret());
+ }
+
+ httpClient.execute(request, response -> {
+ if (response.getCode() >= 400) {
+ throw new PushNotificationException("ntfy notification failed with status " + response.getCode());
+ }
+ return null;
+ });
+ }
+
+ private void sendGotify(UserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws IOException {
+ String serverUrl = Urls.removeTrailingSlash(settings.getPushNotificationServerUrl());
+ String token = settings.getPushNotificationUserSecret();
+
+ if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(token)) {
+ log.warn("gotify notification skipped: missing server URL or token");
+ return;
+ }
+
+ JsonObject json = new JsonObject();
+ json.put("title", feedTitle);
+ json.put("message", entryTitle);
+ json.put("priority", 5);
+ if (StringUtils.isNotBlank(entryUrl)) {
+ json.put("extras",
+ new JsonObject().put("client::notification", new JsonObject().put("click", new JsonObject().put("url", entryUrl))));
+ }
+
+ HttpPost request = new HttpPost(serverUrl + "/message");
+ request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build());
+ request.addHeader("X-Gotify-Key", token);
+ request.setEntity(new StringEntity(json.toString(), ContentType.APPLICATION_JSON));
+
+ httpClient.execute(request, response -> {
+ if (response.getCode() >= 400) {
+ throw new PushNotificationException("gotify notification failed with status " + response.getCode());
+ }
+ return null;
+ });
+ }
+
+ private void sendPushover(UserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws IOException {
+ String token = settings.getPushNotificationUserSecret();
+ String userKey = settings.getPushNotificationUserId();
+
+ if (StringUtils.isBlank(token) || StringUtils.isBlank(userKey)) {
+ log.warn("pushover notification skipped: missing token or user key");
+ return;
+ }
+
+ List params = new ArrayList<>();
+ params.add(new BasicNameValuePair("token", token));
+ params.add(new BasicNameValuePair("user", userKey));
+ params.add(new BasicNameValuePair("title", feedTitle));
+ params.add(new BasicNameValuePair("message", entryTitle));
+ if (StringUtils.isNotBlank(entryUrl)) {
+ params.add(new BasicNameValuePair("url", entryUrl));
+ }
+
+ HttpPost request = new HttpPost("https://api.pushover.net/1/messages.json");
+ request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build());
+ request.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
+
+ httpClient.execute(request, response -> {
+ if (response.getCode() >= 400) {
+ throw new PushNotificationException("pushover notification failed with status " + response.getCode());
+ }
+ return null;
+ });
+ }
+
+ public static class PushNotificationException extends RuntimeException {
+ private static final long serialVersionUID = -3392881821584833819L;
+
+ public PushNotificationException(String message) {
+ super(message);
+ }
+
+ public PushNotificationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java
index 69412cc9..1d1bf062 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java
@@ -52,4 +52,7 @@ public class ServerInfo implements Serializable {
@Schema(required = true)
private int minimumPasswordLength;
+ @Schema(required = true)
+ private boolean pushNotificationsEnabled;
+
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java
index fbc5fa48..8fe6500c 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java
@@ -5,6 +5,7 @@ import java.io.Serializable;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import com.commafeed.backend.model.UserSettings.IconDisplayMode;
+import com.commafeed.backend.model.UserSettings.PushNotificationType;
import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.model.UserSettings.ScrollMode;
@@ -81,6 +82,28 @@ public class Settings implements Serializable {
@Schema(description = "sharing settings", required = true)
private SharingSettings sharingSettings = new SharingSettings();
+ @Schema(description = "push notification settings", required = true)
+ private PushNotificationSettings pushNotificationSettings = new PushNotificationSettings();
+
+ @Schema(description = "User notification settings")
+ @Data
+ public static class PushNotificationSettings implements Serializable {
+ @Schema(description = "notification provider type")
+ private PushNotificationType type;
+
+ @Schema(description = "server URL for ntfy or gotify")
+ private String serverUrl;
+
+ @Schema(description = "user Id")
+ private String userId;
+
+ @Schema(description = "user secret for authentication with the service")
+ private String userSecret;
+
+ @Schema(description = "topic")
+ private String topic;
+ }
+
@Schema(description = "User sharing settings")
@Data
public static class SharingSettings implements Serializable {
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java
index 2075ac24..0522e0f3 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java
@@ -65,6 +65,9 @@ public class Subscription implements Serializable {
@Schema(description = "JEXL legacy filter")
private String filterLegacy;
+ @Schema(description = "whether to send push notifications for new entries of this feed", required = true)
+ private boolean pushNotificationsEnabled;
+
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
FeedCategory category = subscription.getCategory();
Feed feed = subscription.getFeed();
@@ -85,6 +88,7 @@ public class Subscription implements Serializable {
sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
sub.setFilter(subscription.getFilter());
sub.setFilterLegacy(subscription.getFilterLegacy());
+ sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
return sub;
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java
index 05282f32..49aa73e2 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java
@@ -31,4 +31,7 @@ public class FeedModificationRequest implements Serializable {
@Size(max = 4096)
private String filter;
+ @Schema(description = "whether to send push notifications for new entries of this feed")
+ private boolean pushNotificationsEnabled;
+
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java
index 1a73f6af..743279cd 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java
@@ -28,4 +28,7 @@ public class SubscribeRequest implements Serializable {
@Size(max = 128)
private String categoryId;
+ @Schema(description = "whether to send push notifications for new entries of this feed")
+ private boolean pushNotificationsEnabled;
+
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java
index 96a6b490..795f6256 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java
@@ -365,7 +365,8 @@ public class FeedREST {
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
User user = authenticationContext.getCurrentUser();
- long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category);
+ long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0,
+ req.isPushNotificationsEnabled());
return Response.ok(subscriptionId).build();
} catch (Exception e) {
log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e);
@@ -384,7 +385,7 @@ public class FeedREST {
Preconditions.checkNotNull(url);
FeedInfo info = fetchFeedInternal(prependHttp(url));
User user = authenticationContext.getCurrentUser();
- feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle());
+ feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0, false);
} catch (Exception e) {
log.info("Could not subscribe to url {} : {}", url, e.getMessage());
}
@@ -438,6 +439,8 @@ public class FeedREST {
subscription.setFilterLegacy(null);
}
+ subscription.setPushNotificationsEnabled(req.isPushNotificationsEnabled());
+
if (StringUtils.isNotBlank(req.getName())) {
subscription.setTitle(req.getName());
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java
index c782d02f..c3ec5a38 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java
@@ -62,6 +62,7 @@ public class ServerREST {
infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis());
infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired());
infos.setMinimumPasswordLength(config.users().minimumPasswordLength());
+ infos.setPushNotificationsEnabled(config.pushNotifications().enabled());
return infos;
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java
index 8afcde4e..384d10c8 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java
@@ -125,6 +125,12 @@ public class UserREST {
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor());
+
+ s.getPushNotificationSettings().setType(settings.getPushNotificationType());
+ s.getPushNotificationSettings().setServerUrl(settings.getPushNotificationServerUrl());
+ s.getPushNotificationSettings().setUserId(settings.getPushNotificationUserId());
+ s.getPushNotificationSettings().setUserSecret(settings.getPushNotificationUserSecret());
+ s.getPushNotificationSettings().setTopic(settings.getPushNotificationTopic());
} else {
s.setReadingMode(ReadingMode.UNREAD);
s.setReadingOrder(ReadingOrder.DESC);
@@ -190,6 +196,12 @@ public class UserREST {
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor());
+ s.setPushNotificationType(settings.getPushNotificationSettings().getType());
+ s.setPushNotificationServerUrl(settings.getPushNotificationSettings().getServerUrl());
+ s.setPushNotificationUserId(settings.getPushNotificationSettings().getUserId());
+ s.setPushNotificationUserSecret(settings.getPushNotificationSettings().getUserSecret());
+ s.setPushNotificationTopic(settings.getPushNotificationSettings().getTopic());
+
s.setEmail(settings.getSharingSettings().isEmail());
s.setGmail(settings.getSharingSettings().isGmail());
s.setFacebook(settings.getSharingSettings().isFacebook());
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-7.0.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-7.0.xml
index 22b58968..2314273d 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-7.0.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-7.0.xml
@@ -17,5 +17,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java
index b1e4dd21..9de2da33 100644
--- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java
+++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java
@@ -6,6 +6,7 @@ import java.io.OutputStream;
import java.math.BigInteger;
import java.net.NoRouteToHostException;
import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
@@ -56,6 +57,7 @@ class HttpGetterTest {
private CommaFeedConfiguration config;
+ private HttpClientFactory provider;
private HttpGetter getter;
@BeforeEach
@@ -78,7 +80,8 @@ class HttpGetterTest {
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
- this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
+ this.provider = new HttpClientFactory(config, Mockito.mock(CommaFeedVersion.class));
+ this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
}
@ParameterizedTest
@@ -172,7 +175,7 @@ class HttpGetterTest {
@Test
void dataTimeout() {
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500));
- this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
+ this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
@@ -183,7 +186,7 @@ class HttpGetterTest {
@Test
void connectTimeout() {
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500));
- this.getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
+ this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
// try to connect to a non-routable address
// https://stackoverflow.com/a/904609
Exception e = Assertions.assertThrows(Exception.class, () -> getter.get("http://10.255.255.1"));
@@ -367,44 +370,44 @@ class HttpGetterTest {
@BeforeEach
void init() {
Mockito.when(config.httpClient().blockLocalAddresses()).thenReturn(true);
- getter = new HttpGetter(config, () -> NOW, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
+ getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
}
@Test
void localhost() {
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://localhost"));
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://127.0.0.1"));
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://2130706433"));
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://localhost"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://127.0.0.1"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://2130706433"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01"));
}
@Test
void zero() {
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://0.0.0.0"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0.0.0.0"));
}
@Test
void linkLocal() {
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://169.254.12.34"));
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://169.254.169.254"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://169.254.12.34"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://169.254.169.254"));
}
@Test
void multicast() {
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://224.2.3.4"));
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://239.255.255.254"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://224.2.3.4"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://239.255.255.254"));
}
@Test
void privateIpv4Ranges() {
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://10.0.0.1"));
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://172.16.0.1"));
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://192.168.0.1"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://10.0.0.1"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://172.16.0.1"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://192.168.0.1"));
}
@Test
void privateIpv6Ranges() {
- Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://fd12:3456:789a:1::1"));
+ Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://[fe80::215:5dff:fe15:102]"));
}
}
diff --git a/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java
index 1193bc3f..2f49f5bb 100644
--- a/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java
+++ b/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java
@@ -46,7 +46,8 @@ class OPMLImporterTest {
importer.importOpml(user, xml);
Mockito.verify(feedSubscriptionService)
- .subscribe(Mockito.eq(user), Mockito.anyString(), Mockito.anyString(), Mockito.any(FeedCategory.class), Mockito.anyInt());
+ .subscribe(Mockito.eq(user), Mockito.anyString(), Mockito.anyString(), Mockito.any(FeedCategory.class), Mockito.anyInt(),
+ Mockito.anyBoolean());
}
}
diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/PushNotificationServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/PushNotificationServiceTest.java
new file mode 100644
index 00000000..4dae1fdd
--- /dev/null
+++ b/commafeed-server/src/test/java/com/commafeed/backend/service/PushNotificationServiceTest.java
@@ -0,0 +1,136 @@
+package com.commafeed.backend.service;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+import org.mockserver.client.MockServerClient;
+import org.mockserver.junit.jupiter.MockServerExtension;
+import org.mockserver.model.HttpRequest;
+import org.mockserver.model.HttpResponse;
+import org.mockserver.model.JsonBody;
+import org.mockserver.model.MediaType;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.backend.HttpClientFactory;
+import com.commafeed.backend.model.Feed;
+import com.commafeed.backend.model.FeedEntry;
+import com.commafeed.backend.model.FeedEntryContent;
+import com.commafeed.backend.model.FeedSubscription;
+import com.commafeed.backend.model.UserSettings;
+import com.commafeed.backend.model.UserSettings.PushNotificationType;
+
+@ExtendWith(MockServerExtension.class)
+class PushNotificationServiceTest {
+
+ private MockServerClient mockServerClient;
+ private PushNotificationService pushNotificationService;
+ private CommaFeedConfiguration config;
+ private UserSettings userSettings;
+ private FeedSubscription subscription;
+ private FeedEntry entry;
+
+ @BeforeEach
+ void init(MockServerClient mockServerClient) {
+ this.mockServerClient = mockServerClient;
+ this.mockServerClient.reset();
+
+ this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
+ Mockito.when(config.pushNotifications().enabled()).thenReturn(true);
+ Mockito.when(config.pushNotifications().threads()).thenReturn(1);
+ Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30));
+
+ HttpClientFactory httpClientFactory = new HttpClientFactory(config, Mockito.mock(com.commafeed.CommaFeedVersion.class));
+ MetricRegistry metricRegistry = Mockito.mock(MetricRegistry.class);
+ Mockito.when(metricRegistry.meter(Mockito.anyString())).thenReturn(Mockito.mock(Meter.class));
+
+ this.pushNotificationService = new PushNotificationService(httpClientFactory, metricRegistry, config);
+
+ this.userSettings = new UserSettings();
+
+ this.subscription = createSubscription("Test Feed");
+ this.entry = createEntry("Test Entry", "http://example.com/entry");
+ }
+
+ @Test
+ void testNtfyNotification() {
+ userSettings.setPushNotificationType(PushNotificationType.NTFY);
+ userSettings.setPushNotificationServerUrl("http://localhost:" + mockServerClient.getPort());
+ userSettings.setPushNotificationTopic("test-topic");
+ userSettings.setPushNotificationUserSecret("test-secret");
+
+ mockServerClient.when(HttpRequest.request()
+ .withMethod("POST")
+ .withPath("/test-topic")
+ .withHeader("Title", "Test Feed")
+ .withHeader("Click", "http://example.com/entry")
+ .withHeader("Authorization", "Bearer test-secret")
+ .withBody("Test Entry")).respond(HttpResponse.response().withStatusCode(200));
+
+ Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry));
+ }
+
+ @Test
+ void testGotifyNotification() {
+ userSettings.setPushNotificationType(PushNotificationType.GOTIFY);
+ userSettings.setPushNotificationServerUrl("http://localhost:" + mockServerClient.getPort());
+ userSettings.setPushNotificationUserSecret("gotify-token");
+
+ mockServerClient.when(HttpRequest.request()
+ .withMethod("POST")
+ .withPath("/message")
+ .withHeader("X-Gotify-Key", "gotify-token")
+ .withContentType(MediaType.APPLICATION_JSON_UTF_8)
+ .withBody(JsonBody.json("""
+ {
+ "title": "Test Feed",
+ "message": "Test Entry",
+ "priority": 5,
+ "extras": {
+ "client::notification": {
+ "click": {
+ "url": "http://example.com/entry"
+ }
+ }
+ }
+ }
+ """))).respond(HttpResponse.response().withStatusCode(200));
+
+ Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry));
+ }
+
+ @Test
+ void testPushNotificationDisabled() {
+ Mockito.when(config.pushNotifications().enabled()).thenReturn(false);
+ userSettings.setPushNotificationType(PushNotificationType.NTFY);
+ userSettings.setPushNotificationServerUrl("http://localhost:" + mockServerClient.getPort());
+ userSettings.setPushNotificationTopic("test-topic");
+
+ Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry));
+ mockServerClient.verifyZeroInteractions();
+ }
+
+ private static FeedSubscription createSubscription(String title) {
+ FeedSubscription subscription = new FeedSubscription();
+ subscription.setTitle(title);
+ subscription.setFeed(new Feed());
+
+ return subscription;
+ }
+
+ private static FeedEntry createEntry(String title, String url) {
+ FeedEntry entry = new FeedEntry();
+
+ FeedEntryContent content = new FeedEntryContent();
+ content.setTitle(title);
+
+ entry.setContent(content);
+ entry.setUrl(url);
+ return entry;
+ }
+}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java
index 8dbaaf90..09852af9 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java
@@ -39,6 +39,7 @@ public abstract class BaseIT {
private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/");
+ @Getter
private MockServerClient mockServerClient;
private Client client;
private String feedUrl;
@@ -122,10 +123,15 @@ public abstract class BaseIT {
}
protected Long subscribe(String feedUrl, String categoryId) {
+ return subscribe(feedUrl, categoryId, false);
+ }
+
+ protected Long subscribe(String feedUrl, String categoryId, boolean pushNotificationsEnabled) {
SubscribeRequest subscribeRequest = new SubscribeRequest();
subscribeRequest.setUrl(feedUrl);
subscribeRequest.setTitle("my title for this feed");
subscribeRequest.setCategoryId(categoryId);
+ subscribeRequest.setPushNotificationsEnabled(pushNotificationsEnabled);
return RestAssured.given()
.body(subscribeRequest)
.contentType(ContentType.JSON)
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/PushNotificationIT.java b/commafeed-server/src/test/java/com/commafeed/integration/PushNotificationIT.java
new file mode 100644
index 00000000..0e837ef0
--- /dev/null
+++ b/commafeed-server/src/test/java/com/commafeed/integration/PushNotificationIT.java
@@ -0,0 +1,57 @@
+package com.commafeed.integration;
+
+import java.time.Duration;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.model.HttpRequest;
+import org.mockserver.model.HttpResponse;
+import org.mockserver.verify.VerificationTimes;
+
+import com.commafeed.TestConstants;
+import com.commafeed.backend.model.UserSettings.PushNotificationType;
+import com.commafeed.frontend.model.Settings;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+
+@QuarkusTest
+class PushNotificationIT extends BaseIT {
+
+ @BeforeEach
+ void setup() {
+ initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
+ RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
+ }
+
+ @AfterEach
+ void tearDown() {
+ RestAssured.reset();
+ }
+
+ @Test
+ void receivedPushNotifications() {
+ // mock ntfy server
+ HttpRequest ntfyPost = HttpRequest.request().withMethod("POST").withPath("/ntfy/integration-test");
+ getMockServerClient().when(ntfyPost).respond(HttpResponse.response().withStatusCode(200));
+
+ // enable push notifications
+ Settings settings = RestAssured.given().get("rest/user/settings").then().extract().as(Settings.class);
+ settings.getPushNotificationSettings().setType(PushNotificationType.NTFY);
+ settings.getPushNotificationSettings().setServerUrl("http://localhost:" + getMockServerClient().getPort() + "/ntfy");
+ settings.getPushNotificationSettings().setTopic("integration-test");
+ RestAssured.given().body(settings).contentType(ContentType.JSON).post("rest/user/settings").then().statusCode(200);
+
+ // subscribe with push notifications enabled
+ subscribe(getFeedUrl(), null, true);
+
+ // await push notification for the two entries in the feed
+ Awaitility.await()
+ .atMost(Duration.ofSeconds(20))
+ .untilAsserted(() -> getMockServerClient().verify(ntfyPost, VerificationTimes.exactly(2)));
+ }
+
+}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java
index bfc09536..9ea51fa1 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java
@@ -23,6 +23,7 @@ class ServerIT extends BaseIT {
Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval());
Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration());
Assertions.assertEquals(4, serverInfos.getMinimumPasswordLength());
+ Assertions.assertTrue(serverInfos.isPushNotificationsEnabled());
}
}