This commit is contained in:
Athou
2026-02-18 16:03:43 +01:00
parent 77bb948bf2
commit 2be61e8b1c
73 changed files with 1691 additions and 1556 deletions

View File

@@ -17,6 +17,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc
- REST API - REST API
- Fever-compatible API for native mobile apps - Fever-compatible API for native mobile apps
- Can automatically mark articles as read based on user-defined rules - 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 - Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
- [Browser extension](https://github.com/Athou/commafeed-browser-extension) - [Browser extension](https://github.com/Athou/commafeed-browser-extension)
- Compiles to native code for blazing fast startup and low memory usage - Compiles to native code for blazing fast startup and low memory usage

View File

@@ -27,7 +27,7 @@ const createFeed = (id: number, unread: number): Subscription => ({
feedUrl: "", feedUrl: "",
feedLink: "", feedLink: "",
iconUrl: "", iconUrl: "",
notifyOnNewEntries: true, pushNotificationsEnabled: true,
}) })
const root = createCategory("root") const root = createCategory("root")

View File

@@ -29,7 +29,7 @@ export interface Subscription {
newestItemTime?: number newestItemTime?: number
filter?: string filter?: string
filterLegacy?: string filterLegacy?: string
notifyOnNewEntries: boolean pushNotificationsEnabled: boolean
} }
export interface Category { export interface Category {
@@ -111,7 +111,7 @@ export interface FeedModificationRequest {
categoryId?: string categoryId?: string
position?: number position?: number
filter?: string filter?: string
notifyOnNewEntries?: boolean pushNotificationsEnabled: boolean
} }
export interface GetEntriesRequest { export interface GetEntriesRequest {
@@ -238,6 +238,7 @@ export interface ServerInfo {
forceRefreshCooldownDuration: number forceRefreshCooldownDuration: number
initialSetupRequired: boolean initialSetupRequired: boolean
minimumPasswordLength: number minimumPasswordLength: number
pushNotificationsEnabled: boolean
} }
export interface SharingSettings { export interface SharingSettings {
@@ -251,14 +252,13 @@ export interface SharingSettings {
buffer: boolean buffer: boolean
} }
export type NotificationService = "disabled" | "ntfy" | "gotify" | "pushover" export type PushNotificationType = "ntfy" | "gotify" | "pushover"
export interface NotificationSettings { export interface PushNotificationSettings {
enabled: boolean type?: PushNotificationType
type?: Exclude<NotificationService, "disabled">
serverUrl?: string serverUrl?: string
token?: string userId?: string
userKey?: string userSecret?: string
topic?: string topic?: string
} }
@@ -284,7 +284,7 @@ export interface Settings {
disablePullToRefresh: boolean disablePullToRefresh: boolean
primaryColor?: string primaryColor?: string
sharingSettings: SharingSettings sharingSettings: SharingSettings
notificationSettings: NotificationSettings pushNotificationSettings: PushNotificationSettings
} }
export interface LocalSettings { export interface LocalSettings {
@@ -304,7 +304,7 @@ export interface SubscribeRequest {
url: string url: string
title: string title: string
categoryId?: string categoryId?: string
notifyOnNewEntries: boolean pushNotificationsEnabled: boolean
} }
export interface TagRequest { export interface TagRequest {

View File

@@ -151,10 +151,7 @@ export const userSlice = createSlice({
}) })
builder.addCase(changeNotificationSettings.pending, (state, action) => { builder.addCase(changeNotificationSettings.pending, (state, action) => {
if (!state.settings) return if (!state.settings) return
state.settings.notificationSettings = { state.settings.pushNotificationSettings = action.meta.arg
...state.settings.notificationSettings,
...action.meta.arg,
}
}) })
builder.addMatcher( builder.addMatcher(

View File

@@ -1,7 +1,7 @@
import { createAppAsyncThunk } from "@/app/async-thunk" import { createAppAsyncThunk } from "@/app/async-thunk"
import { client } from "@/app/client" import { client } from "@/app/client"
import { reloadEntries } from "@/app/entries/thunks" import { reloadEntries } from "@/app/entries/thunks"
import type { IconDisplayMode, NotificationSettings, 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)) export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
@@ -160,15 +160,12 @@ export const changeSharingSetting = createAppAsyncThunk(
export const changeNotificationSettings = createAppAsyncThunk( export const changeNotificationSettings = createAppAsyncThunk(
"settings/notificationSettings", "settings/notificationSettings",
(notificationUpdate: Partial<NotificationSettings>, thunkApi) => { (pushNotificationSettings: PushNotificationSettings, thunkApi) => {
const { settings } = thunkApi.getState().user const { settings } = thunkApi.getState().user
if (!settings) return if (!settings) return
client.user.saveSettings({ client.user.saveSettings({
...settings, ...settings,
notificationSettings: { pushNotificationSettings,
...settings.notificationSettings,
...notificationUpdate,
},
}) })
} }
) )

View File

@@ -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 = <Trans>Push notifications are not enabled on this CommaFeed instance.</Trans>
} else if (!pushNotificationsConfigured) {
description = <Trans>Push notifications are not configured in your user settings.</Trans>
}
return <Checkbox label={<Trans>Receive push notifications</Trans>} disabled={disabled} description={description} {...props} />
}

View File

@@ -1,5 +1,5 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Button, Checkbox, Group, Stack, Stepper, TextInput } from "@mantine/core" import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { useState } from "react" import { useState } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
@@ -11,6 +11,7 @@ import { useAppDispatch } from "@/app/store"
import { reloadTree } from "@/app/tree/thunks" import { reloadTree } from "@/app/tree/thunks"
import type { FeedInfoRequest, SubscribeRequest } from "@/app/types" import type { FeedInfoRequest, SubscribeRequest } from "@/app/types"
import { Alert } from "@/components/Alert" import { Alert } from "@/components/Alert"
import { ReceivePushNotificationsChechbox } from "@/components/ReceivePushNotificationsChechbox"
import { CategorySelect } from "./CategorySelect" import { CategorySelect } from "./CategorySelect"
export function Subscribe() { export function Subscribe() {
@@ -28,7 +29,7 @@ export function Subscribe() {
url: "", url: "",
title: "", title: "",
categoryId: Constants.categories.all.id, categoryId: Constants.categories.all.id,
notifyOnNewEntries: true, pushNotificationsEnabled: false,
}, },
}) })
@@ -104,9 +105,8 @@ export function Subscribe() {
<TextInput label={<Trans>Feed URL</Trans>} {...step1Form.getInputProps("url")} disabled /> <TextInput label={<Trans>Feed URL</Trans>} {...step1Form.getInputProps("url")} disabled />
<TextInput label={<Trans>Feed name</Trans>} {...step1Form.getInputProps("title")} required autoFocus /> <TextInput label={<Trans>Feed name</Trans>} {...step1Form.getInputProps("title")} required autoFocus />
<CategorySelect label={<Trans>Category</Trans>} {...step1Form.getInputProps("categoryId")} clearable /> <CategorySelect label={<Trans>Category</Trans>} {...step1Form.getInputProps("categoryId")} clearable />
<Checkbox <ReceivePushNotificationsChechbox
label={<Trans>Receive notifications</Trans>} {...step1Form.getInputProps("pushNotificationsEnabled", { type: "checkbox" })}
{...step1Form.getInputProps("notifyOnNewEntries", { type: "checkbox" })}
/> />
</Stack> </Stack>
</Stepper.Step> </Stepper.Step>

View File

@@ -1,125 +0,0 @@
import { Trans } from "@lingui/react/macro"
import { Select, Stack, TextInput } from "@mantine/core"
import { useEffect, useState } from "react"
import { useAppDispatch, useAppSelector } from "@/app/store"
import type { NotificationService as NotificationServiceType, NotificationSettings as NotificationSettingsType } from "@/app/types"
import { changeNotificationSettings } from "@/app/user/thunks"
function useDebouncedSave(value: string, settingsKey: string, dispatch: ReturnType<typeof useAppDispatch>) {
const [localValue, setLocalValue] = useState(value)
useEffect(() => {
setLocalValue(value)
}, [value])
const onBlur = async () => {
if (localValue !== value) {
await dispatch(changeNotificationSettings({ [settingsKey]: localValue }))
}
}
return { localValue, setLocalValue, onBlur }
}
function toServiceValue(settings?: NotificationSettingsType): NotificationServiceType {
if (settings?.enabled && settings.type) {
return settings.type
}
return "disabled"
}
export function NotificationSettings() {
const notificationSettings = useAppSelector(state => state.user.settings?.notificationSettings)
const dispatch = useAppDispatch()
const serviceValue = toServiceValue(notificationSettings)
const serverUrl = useDebouncedSave(notificationSettings?.serverUrl ?? "", "serverUrl", dispatch)
const token = useDebouncedSave(notificationSettings?.token ?? "", "token", dispatch)
const userKey = useDebouncedSave(notificationSettings?.userKey ?? "", "userKey", dispatch)
const topic = useDebouncedSave(notificationSettings?.topic ?? "", "topic", dispatch)
const onServiceChange = async (value: string | null) => {
if (value === "disabled" || !value) {
await dispatch(changeNotificationSettings({ enabled: false, type: undefined }))
} else {
await dispatch(changeNotificationSettings({ enabled: true, type: value as Exclude<NotificationServiceType, "disabled"> }))
}
}
return (
<Stack>
<Select
label={<Trans>Notification service</Trans>}
data={[
{ value: "disabled", label: "Disabled" },
{ value: "ntfy", label: "ntfy" },
{ value: "gotify", label: "Gotify" },
{ value: "pushover", label: "Pushover" },
]}
value={serviceValue}
onChange={onServiceChange}
/>
{serviceValue === "ntfy" && (
<>
<TextInput
label={<Trans>Server URL</Trans>}
placeholder="https://ntfy.sh"
value={serverUrl.localValue}
onChange={e => serverUrl.setLocalValue(e.currentTarget.value)}
onBlur={serverUrl.onBlur}
/>
<TextInput
label={<Trans>Topic</Trans>}
placeholder="commafeed"
value={topic.localValue}
onChange={e => topic.setLocalValue(e.currentTarget.value)}
onBlur={topic.onBlur}
/>
<TextInput
label={<Trans>Access token (optional)</Trans>}
value={token.localValue}
onChange={e => token.setLocalValue(e.currentTarget.value)}
onBlur={token.onBlur}
/>
</>
)}
{serviceValue === "gotify" && (
<>
<TextInput
label={<Trans>Server URL</Trans>}
placeholder="https://gotify.example.com"
value={serverUrl.localValue}
onChange={e => serverUrl.setLocalValue(e.currentTarget.value)}
onBlur={serverUrl.onBlur}
/>
<TextInput
label={<Trans>App token</Trans>}
value={token.localValue}
onChange={e => token.setLocalValue(e.currentTarget.value)}
onBlur={token.onBlur}
/>
</>
)}
{serviceValue === "pushover" && (
<>
<TextInput
label={<Trans>API token</Trans>}
value={token.localValue}
onChange={e => token.setLocalValue(e.currentTarget.value)}
onBlur={token.onBlur}
/>
<TextInput
label={<Trans>User key</Trans>}
value={userKey.localValue}
onChange={e => userKey.setLocalValue(e.currentTarget.value)}
onBlur={userKey.onBlur}
/>
</>
)}
</Stack>
)
}

View File

@@ -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<PushNotificationSettingsModel>()
useEffect(() => {
if (notificationSettings) form.initialize(notificationSettings)
}, [form.initialize, notificationSettings])
const handleSubmit = (values: PushNotificationSettingsModel) => {
dispatch(changeNotificationSettings(values))
}
const typeInputProps = form.getInputProps("type")
if (!pushNotificationsEnabled) {
return <Trans>Push notifications are not enabled on this CommaFeed instance.</Trans>
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<Select
label={<Trans>Push notification service</Trans>}
data={[
{ value: "ntfy", label: "ntfy" },
{ value: "gotify", label: "Gotify" },
{ value: "pushover", label: "Pushover" },
]}
clearable
{...typeInputProps}
onChange={value => {
typeInputProps.onChange(value)
form.setFieldValue("serverUrl", "")
form.setFieldValue("topic", "")
form.setFieldValue("userSecret", "")
form.setFieldValue("userId", "")
}}
/>
{form.values.type === "ntfy" && (
<>
<TextInput
label={<Trans>Server URL</Trans>}
placeholder="https://ntfy.sh"
required
{...form.getInputProps("serverUrl")}
/>
<TextInput label={<Trans>Topic</Trans>} placeholder="commafeed" required {...form.getInputProps("topic")} />
<TextInput label={<Trans>Access token</Trans>} {...form.getInputProps("userSecret")} />
</>
)}
{form.values.type === "gotify" && (
<>
<TextInput
label={<Trans>Server URL</Trans>}
placeholder="https://gotify.example.com"
required
{...form.getInputProps("serverUrl")}
/>
<TextInput label={<Trans>App token</Trans>} required {...form.getInputProps("userSecret")} />
</>
)}
{form.values.type === "pushover" && (
<>
<TextInput label={<Trans>User key</Trans>} required {...form.getInputProps("userId")} />
<TextInput label={<Trans>API token</Trans>} required {...form.getInputProps("userSecret")} />
</>
)}
<Group>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftSection={<TbDeviceFloppy size={16} />}>
<Trans>Save</Trans>
</Button>
</Group>
</Stack>
</form>
)
}

View File

@@ -34,8 +34,8 @@ msgstr "<0> هل تحتاج إلى حساب؟ </0> <1> اشترك! </ 1>"
msgid "About" msgid "About"
msgstr "حول" msgstr "حول"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "مفتاح API" msgstr "مفتاح API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "لم يتم العثور على شيء" msgstr "لم يتم العثور على شيء"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "الأقدم أولا" msgstr "الأقدم أولا"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "الملف الشخصي" msgstr "الملف الشخصي"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "إلغاء النجم"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "إلغاء الاشتراك" msgstr "إلغاء الاشتراك"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Necessites un compte?</0><1>Registreu-vos!</1>"
msgid "About" msgid "About"
msgstr "Sobre" msgstr "Sobre"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "Anunci"
msgid "API key" msgid "API key"
msgstr "Clau API" msgstr "Clau API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr "No hi ha opcions de compartició disponibles."
msgid "Nothing found" msgid "Nothing found"
msgstr "No s'ha trobat res" msgstr "No s'ha trobat res"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "el més vell primer" msgstr "el més vell primer"
@@ -827,9 +820,25 @@ msgstr "Color primari"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "Clic dret"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr "Selecciona el següent canal/categoria no llegit"
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "Selecciona el canal/categoria anterior sense llegir" msgstr "Selecciona el canal/categoria anterior sense llegir"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "Canvia la barra lateral"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Commuta l'estat destacat de l'entrada actual" msgstr "Commuta l'estat destacat de l'entrada actual"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Desestrellar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Donar-se de baixa" msgstr "Donar-se de baixa"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Potřebujete účet?</0><1>Zaregistrujte se!</1>"
msgid "About" msgid "About"
msgstr "Asi" msgstr "Asi"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Klíč API" msgstr "Klíč API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Nic nebylo nalezeno" msgstr "Nic nebylo nalezeno"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Nejdříve nejstarší" msgstr "Nejdříve nejstarší"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Odstranit hvězdu"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Odhlásit odběr" msgstr "Odhlásit odběr"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Angen cyfrif?</0><1>Ymunwch!</1>"
msgid "About" msgid "About"
msgstr "Ynghylch" msgstr "Ynghylch"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Allwedd API" msgstr "Allwedd API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Dim wedi'i ddarganfod" msgstr "Dim wedi'i ddarganfod"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Hynaf yn gyntaf" msgstr "Hynaf yn gyntaf"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Proffil" msgstr "Proffil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "dad-seren"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Dad-danysgrifio" msgstr "Dad-danysgrifio"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Har du brug for en konto?</0><1>Tilmeld dig!</1>"
msgid "About" msgid "About"
msgstr "Omkring" msgstr "Omkring"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nøgle" msgstr "API-nøgle"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Intet fundet" msgstr "Intet fundet"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Ældst først" msgstr "Ældst først"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Afmeld" msgstr "Afmeld"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
msgid "About" msgid "About"
msgstr "Über" msgstr "Über"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "Ankündigung"
msgid "API key" msgid "API key"
msgstr "API-Schlüssel" msgstr "API-Schlüssel"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Nichts gefunden" msgstr "Nichts gefunden"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Älteste zuerst" msgstr "Älteste zuerst"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "Rechtsklick"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "Sidebar an- und ausschalten"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Markierungsstatus des aktuellen Eintrags ändern" msgstr "Markierungsstatus des aktuellen Eintrags ändern"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Stern entfernen"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Abbestellen" msgstr "Abbestellen"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,9 +34,9 @@ msgstr "<0>Need an account?</0><1>Sign up!</1>"
msgid "About" msgid "About"
msgstr "About" msgstr "About"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "Access token (optional)" msgstr "Access token"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
@@ -98,11 +98,11 @@ msgstr "Announcement"
msgid "API key" msgid "API key"
msgstr "API key" msgstr "API key"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "API token" msgstr "API token"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "App token" msgstr "App token"
@@ -170,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/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr "No sharing options available."
msgid "Nothing found" msgid "Nothing found"
msgstr "Nothing found" msgstr "Nothing found"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr "Notification service"
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr "Notifications"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Oldest first" msgstr "Oldest first"
@@ -827,10 +820,26 @@ msgstr "Primary color"
msgid "Profile" msgid "Profile"
msgstr "Profile" msgstr "Profile"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" msgstr "Push notification service"
msgstr "Receive notifications"
#: 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
@@ -866,6 +875,7 @@ msgstr "Right click"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr "Select next unread feed/category"
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "Select previous unread feed/category" msgstr "Select previous unread feed/category"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "Server URL" msgstr "Server URL"
@@ -1080,7 +1090,7 @@ msgstr "Toggle sidebar"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Toggle starred status of current entry" msgstr "Toggle starred status of current entry"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "Topic" msgstr "Topic"
@@ -1107,7 +1117,7 @@ msgstr "Unstar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Unsubscribe" msgstr "Unsubscribe"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "User key" msgstr "User key"

View File

@@ -35,8 +35,8 @@ msgstr "<0>¿Necesitas una cuenta?</0><1>¡Regístrate!</1>"
msgid "About" msgid "About"
msgstr "Acerca de" msgstr "Acerca de"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -99,11 +99,11 @@ msgstr "Anuncio"
msgid "API key" msgid "API key"
msgstr "Clave API" msgstr "Clave API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -171,6 +171,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -679,14 +680,6 @@ msgstr "No hay opciones para compartir disponibles."
msgid "Nothing found" msgid "Nothing found"
msgstr "Nada encontrado" msgstr "Nada encontrado"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Las más antiguas primero" msgstr "Las más antiguas primero"
@@ -828,9 +821,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -867,6 +876,7 @@ msgstr "Clic derecho"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -899,8 +909,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1081,7 +1091,7 @@ msgstr "Alternar barra lateral"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Alternar estado destacado de la entrada actual" msgstr "Alternar estado destacado de la entrada actual"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1108,7 +1118,7 @@ msgstr "Desmarcar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Cancelar suscripción" msgstr "Cancelar suscripción"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>به یک حساب نیاز دارید؟</0><1>ثبت نام کنید
msgid "About" msgid "About"
msgstr "در مورد" msgstr "در مورد"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "کلید API" msgstr "کلید API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "چیزی پیدا نشد" msgstr "چیزی پیدا نشد"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "قدیمی ترین اول" msgstr "قدیمی ترین اول"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "نمایه" msgstr "نمایه"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "لغو اشتراک" msgstr "لغو اشتراک"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Tarvitsetko tilin?</0><1>Rekisteröidy!</1>"
msgid "About" msgid "About"
msgstr "Noin" msgstr "Noin"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-avain" msgstr "API-avain"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Mitään ei löytynyt" msgstr "Mitään ei löytynyt"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Vanhin ensin" msgstr "Vanhin ensin"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profiili" msgstr "Profiili"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Poista tähti"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Peruuta tilaus" msgstr "Peruuta tilaus"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Besoin d'un compte ?</0><1>Enregistrez-vous !</1>"
msgid "About" msgid "About"
msgstr "À propos" msgstr "À propos"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "Annonces"
msgid "API key" msgid "API key"
msgstr "Clé API" msgstr "Clé API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr "Aucune option de partage disponible"
msgid "Nothing found" msgid "Nothing found"
msgstr "Aucun résultat" msgstr "Aucun résultat"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Du plus ancien au plus récent" msgstr "Du plus ancien au plus récent"
@@ -827,9 +820,25 @@ msgstr "Couleur d'ambiance"
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "Clic droit"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr "Sélectionner l'article non lu suivant/la catégorie non lue suivante"
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "Sélectionner l'article non lu précédent/la catégorie non lue précédente" msgstr "Sélectionner l'article non lu précédent/la catégorie non lue précédente"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "Montrer/cacher la barre latérale"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Montrer/cacher le statut favori de l'entrée" msgstr "Montrer/cacher le statut favori de l'entrée"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Retirer des favoris"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Se désabonner" msgstr "Se désabonner"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -35,8 +35,8 @@ msgstr "<0>Necesitas unha conta?</0><1>Crea unha!</1>"
msgid "About" msgid "About"
msgstr "Sobre" msgstr "Sobre"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -99,11 +99,11 @@ msgstr "Anuncio"
msgid "API key" msgid "API key"
msgstr "Clave API" msgstr "Clave API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -171,6 +171,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -679,14 +680,6 @@ msgstr "Non hai opcións para poder compartir."
msgid "Nothing found" msgid "Nothing found"
msgstr "Non se atopou nada" msgstr "Non se atopou nada"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Primeiro o máis antigo" msgstr "Primeiro o máis antigo"
@@ -828,9 +821,25 @@ msgstr "Cor destacada"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -867,6 +876,7 @@ msgstr "Botón dereito"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -899,8 +909,8 @@ msgstr "Seleccionar a seguinte canle/categoría sen ler"
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "Seleccionar a canle/categoría anterior sen ler" msgstr "Seleccionar a canle/categoría anterior sen ler"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1081,7 +1091,7 @@ msgstr "Activación do panel lateral"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Activación do marcado con estrela do artigo actual" msgstr "Activación do marcado con estrela do artigo actual"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1108,7 +1118,7 @@ msgstr "Retirar estrela"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Cancelar a subscrición" msgstr "Cancelar a subscrición"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Fiókra van szüksége?</0><1>Regisztráljon!</1>"
msgid "About" msgid "About"
msgstr "Kb" msgstr "Kb"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API kulcs" msgstr "API kulcs"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Semmi sem található" msgstr "Semmi sem található"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "A legidősebb első" msgstr "A legidősebb első"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Leiratkozás" msgstr "Leiratkozás"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Butuh akun?</0><1>Daftar!</1>"
msgid "About" msgid "About"
msgstr "Tentang" msgstr "Tentang"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "kunci API" msgstr "kunci API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Tidak ada yang ditemukan" msgstr "Tidak ada yang ditemukan"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Tertua dulu" msgstr "Tertua dulu"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Hapus bintang"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Berhenti berlangganan" msgstr "Berhenti berlangganan"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Hai bisogno di un account?</0><1>Registrati!</1>"
msgid "About" msgid "About"
msgstr "Circa" msgstr "Circa"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Chiave API" msgstr "Chiave API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Non è stato trovato nulla" msgstr "Non è stato trovato nulla"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Il più vecchio prima" msgstr "Il più vecchio prima"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profilo" msgstr "Profilo"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Elimina le stelle"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Annulla iscrizione" msgstr "Annulla iscrizione"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>アカウントが必要ですか?</0><1>サインアップ!</1>"
msgid "About" msgid "About"
msgstr "About" msgstr "About"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "お知らせ"
msgid "API key" msgid "API key"
msgstr "APIキー" msgstr "APIキー"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr "共有オプションは利用できません。"
msgid "Nothing found" msgid "Nothing found"
msgstr "何も見つかりませんでした" msgstr "何も見つかりませんでした"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "古い順" msgstr "古い順"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "プロフィール" msgstr "プロフィール"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "右クリック"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "サイドバーを切り替える"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "現在のエントリーのスターステータスを切り替える" msgstr "現在のエントリーのスターステータスを切り替える"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "スターを外す"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "退会" msgstr "退会"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>계정이 필요하십니까?</0><1>가입하세요!</1>"
msgid "About" msgid "About"
msgstr "정보" msgstr "정보"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API 키" msgstr "API 키"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "아무것도 찾을 수 없습니다" msgstr "아무것도 찾을 수 없습니다"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "가장 오래된 것부터" msgstr "가장 오래된 것부터"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "프로필" msgstr "프로필"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "별표 제거"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "구독 취소" msgstr "구독 취소"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Perlukan akaun?</0><1>Daftar!</1>"
msgid "About" msgid "About"
msgstr "Mengenai" msgstr "Mengenai"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Kunci API" msgstr "Kunci API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Tiada apa-apa dijumpai" msgstr "Tiada apa-apa dijumpai"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Tertua dahulu" msgstr "Tertua dahulu"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Nyahbintang"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Nyahlanggan" msgstr "Nyahlanggan"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
msgid "About" msgid "About"
msgstr "Omtrent" msgstr "Omtrent"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nøkkel" msgstr "API-nøkkel"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Ingenting funnet" msgstr "Ingenting funnet"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Eldste først" msgstr "Eldste først"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Fjern stjerne"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Avslutt abonnementet" msgstr "Avslutt abonnementet"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Een account nodig?</0><1>Meld je aan!</1>"
msgid "About" msgid "About"
msgstr "Over" msgstr "Over"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-sleutel" msgstr "API-sleutel"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Niets gevonden" msgstr "Niets gevonden"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Oudste eerst" msgstr "Oudste eerst"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profiel" msgstr "Profiel"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Sterren uit"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Afmelden" msgstr "Afmelden"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
msgid "About" msgid "About"
msgstr "Omtrent" msgstr "Omtrent"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nøkkel" msgstr "API-nøkkel"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Ingenting funnet" msgstr "Ingenting funnet"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Eldste først" msgstr "Eldste først"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Fjern stjerne"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Avslutt abonnementet" msgstr "Avslutt abonnementet"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Potrzebujesz konta?</0><1>Zarejestruj się!</1>"
msgid "About" msgid "About"
msgstr "O" msgstr "O"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "klucz API" msgstr "klucz API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Nic nie znaleziono" msgstr "Nic nie znaleziono"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Najstarsze jako pierwsze" msgstr "Najstarsze jako pierwsze"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Anuluj subskrypcję" msgstr "Anuluj subskrypcję"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Precisa de uma conta?</0><1>Inscreva-se!</1>"
msgid "About" msgid "About"
msgstr "Sobre" msgstr "Sobre"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "Aviso"
msgid "API key" msgid "API key"
msgstr "chave de API" msgstr "chave de API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr "Nenhuma opção de compartilhamento disponível"
msgid "Nothing found" msgid "Nothing found"
msgstr "Nada encontrado" msgstr "Nada encontrado"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Mais antigo primeiro" msgstr "Mais antigo primeiro"
@@ -827,9 +820,25 @@ msgstr "Cor primária"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "Clique com o botão direito"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr "Selecionar próximo feed/categoria não lido"
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "Selecionar feed/categoria não lido anterior" msgstr "Selecionar feed/categoria não lido anterior"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "Alternar barra lateral"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Alternar estrela da entrada atual" msgstr "Alternar estrela da entrada atual"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Desestrelar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Cancelar inscrição" msgstr "Cancelar inscrição"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Нужен аккаунт?</0><1>Зарегистрируйтесь!<
msgid "About" msgid "About"
msgstr "О CommaFeed" msgstr "О CommaFeed"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "Объявление"
msgid "API key" msgid "API key"
msgstr "Ключ API" msgstr "Ключ API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Ничего не найдено" msgstr "Ничего не найдено"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Сначала самые старые" msgstr "Сначала самые старые"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Профиль" msgstr "Профиль"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "Правый клик"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "Переключить боковую панель"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Переключение статуса избранное для текущей записи" msgstr "Переключение статуса избранное для текущей записи"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Удалить из избранного"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Отписаться" msgstr "Отписаться"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Potrebujete účet?</0><1>Zaregistrujte sa!</1>"
msgid "About" msgid "About"
msgstr "Asi" msgstr "Asi"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Kľúč API" msgstr "Kľúč API"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Nič sa nenašlo" msgstr "Nič sa nenašlo"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Najprv najstarší" msgstr "Najprv najstarší"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Odobrať hviezdičku"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Zrušte odber" msgstr "Zrušte odber"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Behöver du ett konto?</0><1>Registrera dig!</1>"
msgid "About" msgid "About"
msgstr "Ungefär" msgstr "Ungefär"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nyckel" msgstr "API-nyckel"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Inget hittades" msgstr "Inget hittades"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Äldst först" msgstr "Äldst först"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Avregistrera" msgstr "Avregistrera"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>Bir hesaba mı ihtiyacınız var?</0><1>Kaydolun!</1>"
msgid "About" msgid "About"
msgstr "Hakkında" msgstr "Hakkında"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "Duyuru"
msgid "API key" msgid "API key"
msgstr "API anahtarı" msgstr "API anahtarı"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr ""
msgid "Nothing found" msgid "Nothing found"
msgstr "Hiçbir şey bulunamadı" msgstr "Hiçbir şey bulunamadı"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "Önce en eski" msgstr "Önce en eski"
@@ -827,9 +820,25 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "Sağ tık"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "Kenar çubuğunu göster/gizle"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "Yıldızı kaldır"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Aboneliği iptal et" msgstr "Aboneliği iptal et"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -34,8 +34,8 @@ msgstr "<0>需要一个帐户?</0><1>注册!</1>"
msgid "About" msgid "About"
msgstr "关于" msgstr "关于"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Access token (optional)" msgid "Access token"
msgstr "" msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -98,11 +98,11 @@ msgstr "公告"
msgid "API key" msgid "API key"
msgstr "API 密钥" msgstr "API 密钥"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "API token" msgid "API token"
msgstr "" msgstr ""
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "App token" msgid "App token"
msgstr "" msgstr ""
@@ -170,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -678,14 +679,6 @@ msgstr "没有可用的分享选项"
msgid "Nothing found" msgid "Nothing found"
msgstr "没有找到" msgstr "没有找到"
#: src/components/settings/NotificationSettings.tsx
msgid "Notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Notifications"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Oldest first" msgid "Oldest first"
msgstr "最早的优先" msgstr "最早的优先"
@@ -827,9 +820,25 @@ msgstr "主颜色"
msgid "Profile" msgid "Profile"
msgstr "配置文件" msgstr "配置文件"
#: src/components/content/add/Subscribe.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/FeedDetailsPage.tsx msgid "Push notification service"
msgid "Receive notifications" 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 "" msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
@@ -866,6 +875,7 @@ msgstr "右键单击"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Save" msgid "Save"
@@ -898,8 +908,8 @@ msgstr "选择下一个未读信息流/类别"
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "选择上一个未读信息流/类别" msgstr "选择上一个未读信息流/类别"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL" msgid "Server URL"
msgstr "" msgstr ""
@@ -1080,7 +1090,7 @@ msgstr "切换侧边栏"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "切换当前条目的星标状态" msgstr "切换当前条目的星标状态"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
@@ -1107,7 +1117,7 @@ msgstr "取消星标"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "取消订阅" msgstr "取消订阅"
#: src/components/settings/NotificationSettings.tsx #: src/components/settings/PushNotificationSettings.tsx
msgid "User key" msgid "User key"
msgstr "" msgstr ""

View File

@@ -19,10 +19,8 @@ const shownGauges: Record<string, string> = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Feed Refresh Engine queue size", "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.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.feed.FeedRefreshEngine.updater.active": "Feed Refresh Engine active database update workers",
"com.commafeed.backend.HttpGetter.pool.max": "HttpGetter max pool size", "com.commafeed.backend.feed.FeedRefreshEngine.notifier.active": "Feed Refresh Engine active push notifications workers",
"com.commafeed.backend.HttpGetter.pool.size": "HttpGetter current pool size", "com.commafeed.backend.feed.FeedRefreshEngine.notifier.queue": "Feed Refresh Engine queued push notifications workers",
"com.commafeed.backend.HttpGetter.pool.leased": "HttpGetter active connections",
"com.commafeed.backend.HttpGetter.pool.pending": "HttpGetter waiting for a connection",
"com.commafeed.backend.HttpGetter.cache.size": "HttpGetter cached entries", "com.commafeed.backend.HttpGetter.cache.size": "HttpGetter cached entries",
"com.commafeed.backend.HttpGetter.cache.memoryUsage": "HttpGetter cache memory usage", "com.commafeed.backend.HttpGetter.cache.memoryUsage": "HttpGetter cache memory usage",
"com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users", "com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users",

View File

@@ -3,7 +3,6 @@ import {
Anchor, Anchor,
Box, Box,
Button, Button,
Checkbox,
Code, Code,
Container, Container,
Divider, Divider,
@@ -31,6 +30,7 @@ import { Alert } from "@/components/Alert"
import { CategorySelect } from "@/components/content/add/CategorySelect" import { CategorySelect } from "@/components/content/add/CategorySelect"
import { FilteringExpressionEditor } from "@/components/content/edit/FilteringExpressionEditor" import { FilteringExpressionEditor } from "@/components/content/edit/FilteringExpressionEditor"
import { Loader } from "@/components/Loader" import { Loader } from "@/components/Loader"
import { ReceivePushNotificationsChechbox } from "@/components/ReceivePushNotificationsChechbox"
import { RelativeDate } from "@/components/RelativeDate" import { RelativeDate } from "@/components/RelativeDate"
export function FeedDetailsPage() { export function FeedDetailsPage() {
@@ -143,6 +143,7 @@ export function FeedDetailsPage() {
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required /> <TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
<CategorySelect label={<Trans>Category</Trans>} {...form.getInputProps("categoryId")} clearable /> <CategorySelect label={<Trans>Category</Trans>} {...form.getInputProps("categoryId")} clearable />
<NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} /> <NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} />
<ReceivePushNotificationsChechbox {...form.getInputProps("pushNotificationsEnabled", { type: "checkbox" })} />
<Input.Wrapper <Input.Wrapper
label={<Trans>Filtering expression</Trans>} label={<Trans>Filtering expression</Trans>}
description={ description={
@@ -164,10 +165,6 @@ export function FeedDetailsPage() {
<FilteringExpressionEditor initialValue={feed.filter} onChange={value => form.setFieldValue("filter", value)} /> <FilteringExpressionEditor initialValue={feed.filter} onChange={value => form.setFieldValue("filter", value)} />
</Box> </Box>
</Input.Wrapper> </Input.Wrapper>
<Checkbox
label={<Trans>Receive notifications</Trans>}
{...form.getInputProps("notifyOnNewEntries", { type: "checkbox" })}
/>
<Group> <Group>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>

View File

@@ -3,8 +3,8 @@ import { Container, Tabs } from "@mantine/core"
import { TbBell, TbCode, TbPhoto, TbUser } from "react-icons/tb" import { TbBell, TbCode, TbPhoto, TbUser } from "react-icons/tb"
import { CustomCodeSettings } from "@/components/settings/CustomCodeSettings" import { CustomCodeSettings } from "@/components/settings/CustomCodeSettings"
import { DisplaySettings } from "@/components/settings/DisplaySettings" import { DisplaySettings } from "@/components/settings/DisplaySettings"
import { NotificationSettings } from "@/components/settings/NotificationSettings"
import { ProfileSettings } from "@/components/settings/ProfileSettings" import { ProfileSettings } from "@/components/settings/ProfileSettings"
import { PushNotificationSettings } from "@/components/settings/PushNotificationSettings"
export function SettingsPage() { export function SettingsPage() {
return ( return (
@@ -14,8 +14,8 @@ export function SettingsPage() {
<Tabs.Tab value="display" leftSection={<TbPhoto size={16} />}> <Tabs.Tab value="display" leftSection={<TbPhoto size={16} />}>
<Trans>Display</Trans> <Trans>Display</Trans>
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value="notifications" leftSection={<TbBell size={16} />}> <Tabs.Tab value="push-notifications" leftSection={<TbBell size={16} />}>
<Trans>Notifications</Trans> <Trans>Push notifications</Trans>
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value="customCode" leftSection={<TbCode size={16} />}> <Tabs.Tab value="customCode" leftSection={<TbCode size={16} />}>
<Trans>Custom code</Trans> <Trans>Custom code</Trans>
@@ -29,8 +29,8 @@ export function SettingsPage() {
<DisplaySettings /> <DisplaySettings />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="notifications" pt="xl"> <Tabs.Panel value="push-notifications" pt="xl">
<NotificationSettings /> <PushNotificationSettings />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="customCode" pt="xl"> <Tabs.Panel value="customCode" pt="xl">

View File

@@ -68,6 +68,12 @@ public interface CommaFeedConfiguration {
@ConfigDocSection @ConfigDocSection
FeedRefresh feedRefresh(); FeedRefresh feedRefresh();
/**
* Push notification settings.
*/
@ConfigDocSection
PushNotifications pushNotifications();
/** /**
* Database settings. * Database settings.
*/ */
@@ -242,6 +248,28 @@ public interface CommaFeedConfiguration {
Duration forceRefreshCooldownDuration(); 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 { interface FeedRefreshErrorHandling {
/** /**
* Number of retries before backoff is applied. * Number of retries before backoff is applied.

View File

@@ -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<Header> 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<String, InputStreamFactory> 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();
}
}
}

View File

@@ -2,20 +2,12 @@ package com.commafeed.backend;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.net.UnknownHostException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.InstantSource; import java.time.InstantSource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.SequencedMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import jakarta.ws.rs.core.CacheControl; 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.commons.lang3.StringUtils;
import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver; 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.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.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.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations; import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.utils.DateUtils; import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest; 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.HttpEntity;
import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; 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.apache.hc.core5.util.Timeout;
import org.brotli.dec.BrotliInputStream;
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate; import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.HttpClientCache; import com.commafeed.CommaFeedConfiguration.HttpClientCache;
import com.commafeed.CommaFeedVersion;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@@ -66,8 +44,6 @@ import lombok.Getter;
import lombok.Lombok; import lombok.Lombok;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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 * Smart HTTP getter: handles gzip, ssl, last modified and etag headers
@@ -82,42 +58,27 @@ public class HttpGetter {
private final CloseableHttpClient client; private final CloseableHttpClient client;
private final Cache<HttpRequest, HttpResponse> cache; private final Cache<HttpRequest, HttpResponse> 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.config = config;
this.instantSource = instantSource; this.instantSource = instantSource;
this.client = httpClientFactory.newClient(config.feedRefresh().httpThreads());
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.cache = newCache(config); 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", "size"), () -> cache == null ? 0 : cache.size());
metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "memoryUsage"), metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "memoryUsage"),
() -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> ArrayUtils.getLength(e.content)).sum()); () -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> ArrayUtils.getLength(e.content)).sum());
} }
public HttpResult get(String url) public HttpResult get(String url) throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException {
return get(HttpRequest.builder(url).build()); return get(HttpRequest.builder(url).build());
} }
public HttpResult get(HttpRequest request) public HttpResult get(HttpRequest request)
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException { throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
URI uri = URI.create(request.getUrl()); URI uri = URI.create(request.getUrl());
ensureHttpScheme(uri.getScheme()); ensureHttpScheme(uri.getScheme());
if (config.httpClient().blockLocalAddresses()) {
ensurePublicAddress(uri.getHost());
}
final HttpResponse response; final HttpResponse response;
if (cache == null) { if (cache == null) {
response = invoke(request); 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 { private HttpResponse invoke(HttpRequest request) throws IOException {
log.debug("fetching {}", request.getUrl()); 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<Header> 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<String, InputStreamFactory> 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<HttpRequest, HttpResponse> newCache(CommaFeedConfiguration config) { private static Cache<HttpRequest, HttpResponse> newCache(CommaFeedConfiguration config) {
HttpClientCache cacheConfig = config.httpClient().cache(); HttpClientCache cacheConfig = config.httpClient().cache();
if (!cacheConfig.enabled()) { 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 @Getter
public static class NotModifiedException extends Exception { public static class NotModifiedException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@@ -53,6 +53,10 @@ public class Urls {
} }
public static String removeTrailingSlash(String url) { public static String removeTrailingSlash(String url) {
if (url == null) {
return null;
}
if (url.endsWith("/")) { if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1); url = url.substring(0, url.length() - 1);
} }

View File

@@ -14,7 +14,6 @@ import org.apache.hc.core5.net.URIBuilder;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HostNotAllowedException;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException; import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.SchemeNotAllowedException; import com.commafeed.backend.HttpGetter.SchemeNotAllowedException;
@@ -98,7 +97,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
} }
private byte[] fetchForUser(String googleAuthKey, String userId) 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") URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", PART_SNIPPET) .queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey) .queryParam("key", googleAuthKey)
@@ -108,7 +107,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
} }
private byte[] fetchForChannel(String googleAuthKey, String channelId) 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") URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", PART_SNIPPET) .queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey) .queryParam("key", googleAuthKey)
@@ -118,7 +117,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
} }
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId) 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") URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
.queryParam("part", PART_SNIPPET) .queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey) .queryParam("key", googleAuthKey)

View File

@@ -13,7 +13,6 @@ import org.apache.commons.lang3.Strings;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HostNotAllowedException;
import com.commafeed.backend.HttpGetter.HttpRequest; import com.commafeed.backend.HttpGetter.HttpRequest;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException; import com.commafeed.backend.HttpGetter.NotModifiedException;
@@ -46,7 +45,7 @@ public class FeedFetcher {
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
Instant lastPublishedDate, String lastContentHash) throws FeedParsingException, IOException, NotModifiedException, Instant lastPublishedDate, String lastContentHash) throws FeedParsingException, IOException, NotModifiedException,
TooManyRequestsException, SchemeNotAllowedException, HostNotAllowedException, NoFeedFoundException { TooManyRequestsException, SchemeNotAllowedException, NoFeedFoundException {
log.debug("Fetching feed {}", feedUrl); log.debug("Fetching feed {}", feedUrl);
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build()); HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());

View File

@@ -7,6 +7,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue; import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; 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.dao.UnitOfWork;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -32,6 +35,7 @@ public class FeedRefreshEngine {
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
private final FeedRefreshWorker worker; private final FeedRefreshWorker worker;
private final FeedRefreshUpdater updater; private final FeedRefreshUpdater updater;
private final FeedUpdateNotifier notifier;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private final Meter refill; private final Meter refill;
@@ -42,13 +46,15 @@ public class FeedRefreshEngine {
private final ExecutorService refillExecutor; private final ExecutorService refillExecutor;
private final ThreadPoolExecutor workerExecutor; private final ThreadPoolExecutor workerExecutor;
private final ThreadPoolExecutor databaseUpdaterExecutor; private final ThreadPoolExecutor databaseUpdaterExecutor;
private final ThreadPoolExecutor notifierExecutor;
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater, public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
CommaFeedConfiguration config, MetricRegistry metrics) { FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
this.unitOfWork = unitOfWork; this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO; this.feedDAO = feedDAO;
this.worker = worker; this.worker = worker;
this.updater = updater; this.updater = updater;
this.notifier = notifier;
this.config = config; this.config = config;
this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill")); this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
@@ -59,10 +65,14 @@ public class FeedRefreshEngine {
this.refillExecutor = newDiscardingSingleThreadExecutorService(); this.refillExecutor = newDiscardingSingleThreadExecutorService();
this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads()); this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads()); this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
config.pushNotifications().queueCapacity());
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size); metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount); metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount); metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) notifierExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size());
} }
public void start() { public void start() {
@@ -152,10 +162,19 @@ public class FeedRefreshEngine {
private void processFeedAsync(Feed feed) { private void processFeedAsync(Feed feed) {
CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor) CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
.thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor) .thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
.whenComplete((data, ex) -> { .thenCompose(r -> {
if (ex != null) { List<CompletableFuture<Void>> futures = r.insertedUnreadEntriesBySubscription().entrySet().stream().map(e -> {
FeedSubscription sub = e.getKey();
List<FeedEntry> 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); log.error("error while processing feed {}", feed.getUrl(), ex);
} return null;
}); });
} }
@@ -183,6 +202,7 @@ public class FeedRefreshEngine {
this.refillExecutor.shutdownNow(); this.refillExecutor.shutdownNow();
this.workerExecutor.shutdownNow(); this.workerExecutor.shutdownNow();
this.databaseUpdaterExecutor.shutdownNow(); this.databaseUpdaterExecutor.shutdownNow();
this.notifierExecutor.shutdownNow();
} }
/** /**
@@ -194,6 +214,16 @@ public class FeedRefreshEngine {
return pool; 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 * returns an ExecutorService that blocks submissions until a thread is available
*/ */

View File

@@ -20,19 +20,14 @@ import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.feed.parser.FeedParserResult.Content; import com.commafeed.backend.feed.parser.FeedParserResult.Content;
import com.commafeed.backend.feed.parser.FeedParserResult.Entry; import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService; import com.commafeed.backend.service.FeedService;
import com.commafeed.backend.service.NotificationService;
import com.commafeed.frontend.ws.WebSocketMessageBuilder;
import com.commafeed.frontend.ws.WebSocketSessions;
import com.google.common.util.concurrent.Striped; import com.google.common.util.concurrent.Striped;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -48,9 +43,6 @@ public class FeedRefreshUpdater {
private final FeedService feedService; private final FeedService feedService;
private final FeedEntryService feedEntryService; private final FeedEntryService feedEntryService;
private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedSubscriptionDAO feedSubscriptionDAO;
private final UserSettingsDAO userSettingsDAO;
private final WebSocketSessions webSocketSessions;
private final NotificationService notificationService;
private final Striped<Lock> locks; private final Striped<Lock> locks;
@@ -58,15 +50,11 @@ public class FeedRefreshUpdater {
private final Meter entryInserted; private final Meter entryInserted;
public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics, public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics,
FeedSubscriptionDAO feedSubscriptionDAO, UserSettingsDAO userSettingsDAO, WebSocketSessions webSocketSessions, FeedSubscriptionDAO feedSubscriptionDAO) {
NotificationService notificationService) {
this.unitOfWork = unitOfWork; this.unitOfWork = unitOfWork;
this.feedService = feedService; this.feedService = feedService;
this.feedEntryService = feedEntryService; this.feedEntryService = feedEntryService;
this.feedSubscriptionDAO = feedSubscriptionDAO; this.feedSubscriptionDAO = feedSubscriptionDAO;
this.userSettingsDAO = userSettingsDAO;
this.webSocketSessions = webSocketSessions;
this.notificationService = notificationService;
locks = Striped.lazyWeakLock(100000); locks = Striped.lazyWeakLock(100000);
@@ -100,9 +88,12 @@ public class FeedRefreshUpdater {
if (locked1 && locked2) { if (locked1 && locked2) {
processed = true; processed = true;
insertedEntry = unitOfWork.call(() -> { insertedEntry = unitOfWork.call(() -> {
FeedEntry feedEntry = feedEntryService.find(feed, entry); if (feedEntryService.find(feed, entry) != null) {
if (feedEntry == null) { // entry already exists, nothing to do
feedEntry = feedEntryService.create(feed, entry); return null;
}
FeedEntry feedEntry = feedEntryService.create(feed, entry);
entryInserted.mark(); entryInserted.mark();
for (FeedSubscription sub : subscriptions) { for (FeedSubscription sub : subscriptions) {
boolean unread = feedEntryService.applyFilter(sub, feedEntry); boolean unread = feedEntryService.applyFilter(sub, feedEntry);
@@ -111,8 +102,6 @@ public class FeedRefreshUpdater {
} }
} }
return feedEntry; return feedEntry;
}
return null;
}); });
} else { } else {
log.error("lock timeout for {} - {}", feed.getUrl(), key1); log.error("lock timeout for {} - {}", feed.getUrl(), key1);
@@ -131,11 +120,10 @@ public class FeedRefreshUpdater {
return new AddEntryResult(processed, insertedEntry, subscriptionsForWhichEntryIsUnread); return new AddEntryResult(processed, insertedEntry, subscriptionsForWhichEntryIsUnread);
} }
public boolean update(Feed feed, List<Entry> entries) { public FeedRefreshUpdaterResult update(Feed feed, List<Entry> entries) {
boolean processed = true; boolean processed = true;
long inserted = 0; long inserted = 0;
Map<FeedSubscription, Long> unreadCountBySubscription = new HashMap<>(); Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription = new HashMap<>();
Map<FeedSubscription, List<FeedEntry>> insertedEntriesBySubscription = new HashMap<>();
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
List<FeedSubscription> subscriptions = null; List<FeedSubscription> subscriptions = null;
@@ -147,9 +135,8 @@ public class FeedRefreshUpdater {
processed &= addEntryResult.processed; processed &= addEntryResult.processed;
inserted += addEntryResult.insertedEntry != null ? 1 : 0; inserted += addEntryResult.insertedEntry != null ? 1 : 0;
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> { addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> {
unreadCountBySubscription.merge(sub, 1L, Long::sum);
if (addEntryResult.insertedEntry != null) { if (addEntryResult.insertedEntry != null) {
insertedEntriesBySubscription.computeIfAbsent(sub, k -> new ArrayList<>()).add(addEntryResult.insertedEntry); insertedUnreadEntriesBySubscription.computeIfAbsent(sub, k -> new ArrayList<>()).add(addEntryResult.insertedEntry);
} }
}); });
} }
@@ -172,36 +159,13 @@ public class FeedRefreshUpdater {
unitOfWork.run(() -> feedService.update(feed)); unitOfWork.run(() -> feedService.update(feed));
notifyOverWebsocket(unreadCountBySubscription); return new FeedRefreshUpdaterResult(insertedUnreadEntriesBySubscription);
sendNotifications(insertedEntriesBySubscription);
return processed;
}
private void notifyOverWebsocket(Map<FeedSubscription, Long> unreadCountBySubscription) {
unreadCountBySubscription.forEach((sub, unreadCount) -> webSocketSessions.sendMessage(sub.getUser(),
WebSocketMessageBuilder.newFeedEntries(sub, unreadCount)));
}
private void sendNotifications(Map<FeedSubscription, List<FeedEntry>> insertedEntriesBySubscription) {
insertedEntriesBySubscription.forEach((sub, feedEntries) -> {
if (!sub.isNotifyOnNewEntries()) {
return;
}
try {
UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(sub.getUser()));
if (settings != null && settings.isNotificationEnabled()) {
for (FeedEntry feedEntry : feedEntries) {
notificationService.notify(settings, sub, feedEntry);
}
}
} catch (Exception e) {
log.error("error sending push notification for subscription {}", sub.getId(), e);
}
});
} }
private record AddEntryResult(boolean processed, FeedEntry insertedEntry, Set<FeedSubscription> subscriptionsForWhichEntryIsUnread) { private record AddEntryResult(boolean processed, FeedEntry insertedEntry, Set<FeedSubscription> subscriptionsForWhichEntryIsUnread) {
} }
public record FeedRefreshUpdaterResult(Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription) {
}
} }

View File

@@ -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<FeedEntry> entries) {
if (!entries.isEmpty()) {
webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size()));
}
}
public void sendPushNotifications(FeedSubscription sub, List<FeedEntry> 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);
}
}
}
}

View File

@@ -46,7 +46,7 @@ public class FeedSubscription extends AbstractModel {
@Column(name = "filtering_expression_legacy", length = 4096) @Column(name = "filtering_expression_legacy", length = 4096)
private String filterLegacy; private String filterLegacy;
@Column(name = "notify_on_new_entries", length = 4096) @Column(name = "push_notifications_enabled")
private boolean notifyOnNewEntries = true; private boolean pushNotificationsEnabled;
} }

View File

@@ -77,7 +77,7 @@ public class UserSettings extends AbstractModel {
ON_MOBILE ON_MOBILE
} }
public enum NotificationType { public enum PushNotificationType {
@JsonProperty("ntfy") @JsonProperty("ntfy")
NTFY, NTFY,
@@ -144,23 +144,21 @@ public class UserSettings extends AbstractModel {
private boolean unreadCountFavicon; private boolean unreadCountFavicon;
private boolean disablePullToRefresh; private boolean disablePullToRefresh;
private boolean notificationEnabled;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "notification_type", length = 16) @Column(name = "push_notification_type", length = 16)
private NotificationType notificationType; private PushNotificationType pushNotificationType;
@Column(name = "notification_server_url", length = 1024) @Column(name = "push_notification_server_url", length = 1024)
private String notificationServerUrl; private String pushNotificationServerUrl;
@Column(name = "notification_token", length = 512) @Column(name = "push_notification_user_id", length = 512)
private String notificationToken; private String pushNotificationUserId;
@Column(name = "notification_user_key", length = 512) @Column(name = "push_notification_user_secret", length = 512)
private String notificationUserKey; private String pushNotificationUserSecret;
@Column(name = "notification_topic", length = 256) @Column(name = "push_notification_topic", length = 256)
private String notificationTopic; private String pushNotificationTopic;
private boolean email; private boolean email;
private boolean gmail; private boolean gmail;

View File

@@ -77,7 +77,7 @@ public class OPMLImporter {
} }
// make sure we continue with the import process even if a feed failed // make sure we continue with the import process even if a feed failed
try { try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position, true); feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position, false);
} catch (Exception e) { } catch (Exception e) {
log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage()); log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
} }

View File

@@ -49,7 +49,7 @@ public class FeedSubscriptionService {
}); });
} }
public long subscribe(User user, String url, String title, FeedCategory category, int position, boolean notifyOnNewEntries) { public long subscribe(User user, String url, String title, FeedCategory category, int position, boolean pushNotificationsEnabled) {
Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser(); Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser();
if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= 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)", String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)",
@@ -73,7 +73,7 @@ public class FeedSubscriptionService {
sub.setCategory(category); sub.setCategory(category);
sub.setPosition(position); sub.setPosition(position);
sub.setTitle(FeedUtils.truncate(title, 128)); sub.setTitle(FeedUtils.truncate(title, 128));
sub.setNotifyOnNewEntries(notifyOnNewEntries); sub.setPushNotificationsEnabled(pushNotificationsEnabled);
return feedSubscriptionDAO.merge(sub).getId(); return feedSubscriptionDAO.merge(sub).getId();
} }

View File

@@ -1,171 +0,0 @@
package com.commafeed.backend.service;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings;
import lombok.extern.slf4j.Slf4j;
@Singleton
@Slf4j
public class NotificationService {
private final HttpClient httpClient;
public NotificationService() {
this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
}
public NotificationService(HttpClient httpClient) {
this.httpClient = httpClient;
}
public void notify(UserSettings settings, FeedSubscription subscription, FeedEntry entry) {
if (!settings.isNotificationEnabled() || settings.getNotificationType() == null) {
return;
}
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.getNotificationType()) {
case NTFY -> sendNtfy(settings, feedTitle, entryTitle, entryUrl);
case GOTIFY -> sendGotify(settings, feedTitle, entryTitle, entryUrl);
case PUSHOVER -> sendPushover(settings, feedTitle, entryTitle, entryUrl);
default -> log.warn("unknown notification type: {}", settings.getNotificationType());
}
} catch (Exception e) {
log.error("failed to send {} notification for entry '{}' in feed '{}'", settings.getNotificationType(), entryTitle, feedTitle,
e);
}
}
private void sendNtfy(UserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws Exception {
String serverUrl = stripTrailingSlash(settings.getNotificationServerUrl());
String topic = settings.getNotificationTopic();
if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(topic)) {
log.warn("ntfy notification skipped: missing server URL or topic");
return;
}
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(serverUrl + "/" + topic))
.timeout(Duration.ofSeconds(10))
.header("Title", feedTitle + ": " + entryTitle)
.POST(BodyPublishers.ofString(entryTitle));
if (StringUtils.isNotBlank(entryUrl)) {
builder.header("Click", entryUrl);
}
if (StringUtils.isNotBlank(settings.getNotificationToken())) {
builder.header("Authorization", "Bearer " + settings.getNotificationToken());
}
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
log.error("ntfy notification failed with status {}: {}", response.statusCode(), response.body());
}
}
private void sendGotify(UserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws Exception {
String serverUrl = stripTrailingSlash(settings.getNotificationServerUrl());
String token = settings.getNotificationToken();
if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(token)) {
log.warn("gotify notification skipped: missing server URL or token");
return;
}
String message = entryTitle;
if (StringUtils.isNotBlank(entryUrl)) {
message += "\n" + entryUrl;
}
String json = """
{"title":"%s","message":"%s","priority":5,"extras":{"client::notification":{"click":{"url":"%s"}}}}"""
.formatted(escapeJson(feedTitle), escapeJson(message), escapeJson(StringUtils.defaultString(entryUrl)));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(serverUrl + "/message"))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.header("X-Gotify-Key", token)
.POST(BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
log.error("gotify notification failed with status {}: {}", response.statusCode(), response.body());
}
}
private void sendPushover(UserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws Exception {
String token = settings.getNotificationToken();
String userKey = settings.getNotificationUserKey();
if (StringUtils.isBlank(token) || StringUtils.isBlank(userKey)) {
log.warn("pushover notification skipped: missing token or user key");
return;
}
StringBuilder body = new StringBuilder();
body.append("token=").append(urlEncode(token));
body.append("&user=").append(urlEncode(userKey));
body.append("&title=").append(urlEncode(feedTitle));
body.append("&message=").append(urlEncode(entryTitle));
if (StringUtils.isNotBlank(entryUrl)) {
body.append("&url=").append(urlEncode(entryUrl));
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.pushover.net/1/messages.json"))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(BodyPublishers.ofString(body.toString()))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
log.error("pushover notification failed with status {}: {}", response.statusCode(), response.body());
}
}
private static String stripTrailingSlash(String url) {
if (url != null && url.endsWith("/")) {
return url.substring(0, url.length() - 1);
}
return url;
}
private static String urlEncode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private static String escapeJson(String value) {
if (value == null) {
return "";
}
return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
}
}

View File

@@ -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<NameValuePair> 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);
}
}
}

View File

@@ -52,4 +52,7 @@ public class ServerInfo implements Serializable {
@Schema(required = true) @Schema(required = true)
private int minimumPasswordLength; private int minimumPasswordLength;
@Schema(required = true)
private boolean pushNotificationsEnabled;
} }

View File

@@ -5,6 +5,7 @@ import java.io.Serializable;
import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.media.Schema;
import com.commafeed.backend.model.UserSettings.IconDisplayMode; 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.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.model.UserSettings.ScrollMode; import com.commafeed.backend.model.UserSettings.ScrollMode;
@@ -81,28 +82,25 @@ public class Settings implements Serializable {
@Schema(description = "sharing settings", required = true) @Schema(description = "sharing settings", required = true)
private SharingSettings sharingSettings = new SharingSettings(); private SharingSettings sharingSettings = new SharingSettings();
@Schema(description = "notification settings", required = true) @Schema(description = "push notification settings", required = true)
private NotificationSettings notificationSettings = new NotificationSettings(); private PushNotificationSettings pushNotificationSettings = new PushNotificationSettings();
@Schema(description = "User notification settings") @Schema(description = "User notification settings")
@Data @Data
public static class NotificationSettings implements Serializable { public static class PushNotificationSettings implements Serializable {
@Schema(required = true) @Schema(description = "notification provider type")
private boolean enabled; private PushNotificationType type;
@Schema(description = "notification provider type: ntfy, gotify, or pushover")
private String type;
@Schema(description = "server URL for ntfy or gotify") @Schema(description = "server URL for ntfy or gotify")
private String serverUrl; private String serverUrl;
@Schema(description = "API token for gotify or pushover") @Schema(description = "user Id")
private String token; private String userId;
@Schema(description = "user key for pushover") @Schema(description = "user secret for authentication with the service")
private String userKey; private String userSecret;
@Schema(description = "topic for ntfy") @Schema(description = "topic")
private String topic; private String topic;
} }

View File

@@ -65,8 +65,8 @@ public class Subscription implements Serializable {
@Schema(description = "JEXL legacy filter") @Schema(description = "JEXL legacy filter")
private String filterLegacy; private String filterLegacy;
@Schema(description = "whether to send notifications for new entries of this feed", required = true) @Schema(description = "whether to send push notifications for new entries of this feed", required = true)
private boolean notifyOnNewEntries; private boolean pushNotificationsEnabled;
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) { public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
FeedCategory category = subscription.getCategory(); FeedCategory category = subscription.getCategory();
@@ -88,7 +88,7 @@ public class Subscription implements Serializable {
sub.setCategoryId(category == null ? null : String.valueOf(category.getId())); sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
sub.setFilter(subscription.getFilter()); sub.setFilter(subscription.getFilter());
sub.setFilterLegacy(subscription.getFilterLegacy()); sub.setFilterLegacy(subscription.getFilterLegacy());
sub.setNotifyOnNewEntries(subscription.isNotifyOnNewEntries()); sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
return sub; return sub;
} }

View File

@@ -31,7 +31,7 @@ public class FeedModificationRequest implements Serializable {
@Size(max = 4096) @Size(max = 4096)
private String filter; private String filter;
@Schema(description = "whether to send notifications for new entries of this feed") @Schema(description = "whether to send push notifications for new entries of this feed")
private Boolean notifyOnNewEntries; private boolean pushNotificationsEnabled;
} }

View File

@@ -28,7 +28,7 @@ public class SubscribeRequest implements Serializable {
@Size(max = 128) @Size(max = 128)
private String categoryId; private String categoryId;
@Schema(description = "whether to send notifications for new entries of this feed") @Schema(description = "whether to send push notifications for new entries of this feed")
private boolean notifyOnNewEntries = true; private boolean pushNotificationsEnabled;
} }

View File

@@ -366,7 +366,7 @@ public class FeedREST {
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl())); FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0, long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0,
req.isNotifyOnNewEntries()); req.isPushNotificationsEnabled());
return Response.ok(subscriptionId).build(); return Response.ok(subscriptionId).build();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e); log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e);
@@ -385,7 +385,7 @@ public class FeedREST {
Preconditions.checkNotNull(url); Preconditions.checkNotNull(url);
FeedInfo info = fetchFeedInternal(prependHttp(url)); FeedInfo info = fetchFeedInternal(prependHttp(url));
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0, true); feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0, false);
} catch (Exception e) { } catch (Exception e) {
log.info("Could not subscribe to url {} : {}", url, e.getMessage()); log.info("Could not subscribe to url {} : {}", url, e.getMessage());
} }
@@ -439,9 +439,7 @@ public class FeedREST {
subscription.setFilterLegacy(null); subscription.setFilterLegacy(null);
} }
if (req.getNotifyOnNewEntries() != null) { subscription.setPushNotificationsEnabled(req.isPushNotificationsEnabled());
subscription.setNotifyOnNewEntries(req.getNotifyOnNewEntries());
}
if (StringUtils.isNotBlank(req.getName())) { if (StringUtils.isNotBlank(req.getName())) {
subscription.setTitle(req.getName()); subscription.setTitle(req.getName());

View File

@@ -62,6 +62,7 @@ public class ServerREST {
infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis()); infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis());
infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired()); infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired());
infos.setMinimumPasswordLength(config.users().minimumPasswordLength()); infos.setMinimumPasswordLength(config.users().minimumPasswordLength());
infos.setPushNotificationsEnabled(config.pushNotifications().enabled());
return infos; return infos;
} }

View File

@@ -43,7 +43,6 @@ import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.model.UserSettings.IconDisplayMode; import com.commafeed.backend.model.UserSettings.IconDisplayMode;
import com.commafeed.backend.model.UserSettings.NotificationType;
import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.model.UserSettings.ScrollMode; import com.commafeed.backend.model.UserSettings.ScrollMode;
@@ -127,14 +126,11 @@ public class UserREST {
s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor()); s.setPrimaryColor(settings.getPrimaryColor());
s.getNotificationSettings().setEnabled(settings.isNotificationEnabled()); s.getPushNotificationSettings().setType(settings.getPushNotificationType());
if (settings.getNotificationType() != null) { s.getPushNotificationSettings().setServerUrl(settings.getPushNotificationServerUrl());
s.getNotificationSettings().setType(settings.getNotificationType().name().toLowerCase()); s.getPushNotificationSettings().setUserId(settings.getPushNotificationUserId());
} s.getPushNotificationSettings().setUserSecret(settings.getPushNotificationUserSecret());
s.getNotificationSettings().setServerUrl(settings.getNotificationServerUrl()); s.getPushNotificationSettings().setTopic(settings.getPushNotificationTopic());
s.getNotificationSettings().setToken(settings.getNotificationToken());
s.getNotificationSettings().setUserKey(settings.getNotificationUserKey());
s.getNotificationSettings().setTopic(settings.getNotificationTopic());
} else { } else {
s.setReadingMode(ReadingMode.UNREAD); s.setReadingMode(ReadingMode.UNREAD);
s.setReadingOrder(ReadingOrder.DESC); s.setReadingOrder(ReadingOrder.DESC);
@@ -200,16 +196,11 @@ public class UserREST {
s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor()); s.setPrimaryColor(settings.getPrimaryColor());
s.setNotificationEnabled(settings.getNotificationSettings().isEnabled()); s.setPushNotificationType(settings.getPushNotificationSettings().getType());
if (settings.getNotificationSettings().getType() != null) { s.setPushNotificationServerUrl(settings.getPushNotificationSettings().getServerUrl());
s.setNotificationType(NotificationType.valueOf(settings.getNotificationSettings().getType().toUpperCase())); s.setPushNotificationUserId(settings.getPushNotificationSettings().getUserId());
} else { s.setPushNotificationUserSecret(settings.getPushNotificationSettings().getUserSecret());
s.setNotificationType(null); s.setPushNotificationTopic(settings.getPushNotificationSettings().getTopic());
}
s.setNotificationServerUrl(settings.getNotificationSettings().getServerUrl());
s.setNotificationToken(settings.getNotificationSettings().getToken());
s.setNotificationUserKey(settings.getNotificationSettings().getUserKey());
s.setNotificationTopic(settings.getNotificationSettings().getTopic());
s.setEmail(settings.getSharingSettings().isEmail()); s.setEmail(settings.getSharingSettings().isEmail());
s.setGmail(settings.getSharingSettings().isGmail()); s.setGmail(settings.getSharingSettings().isGmail());

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="add-subscription-notify-on-new-entries" author="commafeed">
<addColumn tableName="FEEDSUBSCRIPTIONS">
<column name="notify_on_new_entries" type="BOOLEAN" valueBoolean="true">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>
<changeSet id="add-notification-settings" author="commafeed">
<addColumn tableName="USERSETTINGS">
<column name="notificationEnabled" type="BOOLEAN" valueBoolean="false">
<constraints nullable="false" />
</column>
<column name="notification_type" type="VARCHAR(16)" />
<column name="notification_server_url" type="VARCHAR(1024)" />
<column name="notification_token" type="VARCHAR(512)" />
<column name="notification_user_key" type="VARCHAR(512)" />
<column name="notification_topic" type="VARCHAR(256)" />
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -17,5 +17,20 @@
</update> </update>
</changeSet> </changeSet>
<changeSet id="add-push-notification-settings" author="athou">
<addColumn tableName="USERSETTINGS">
<column name="push_notification_type" type="VARCHAR(16)" />
<column name="push_notification_server_url" type="VARCHAR(1024)" />
<column name="push_notification_user_id" type="VARCHAR(512)" />
<column name="push_notification_user_secret" type="VARCHAR(512)" />
<column name="push_notification_topic" type="VARCHAR(256)" />
</addColumn>
<addColumn tableName="FEEDSUBSCRIPTIONS">
<column name="push_notifications_enabled" type="BOOLEAN" valueBoolean="false">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -37,7 +37,6 @@
<include file="changelogs/db.changelog-5.8.xml" /> <include file="changelogs/db.changelog-5.8.xml" />
<include file="changelogs/db.changelog-5.11.xml" /> <include file="changelogs/db.changelog-5.11.xml" />
<include file="changelogs/db.changelog-5.12.xml" /> <include file="changelogs/db.changelog-5.12.xml" />
<include file="changelogs/db.changelog-6.1.xml" />
<include file="changelogs/db.changelog-7.0.xml" /> <include file="changelogs/db.changelog-7.0.xml" />
</databaseChangeLog> </databaseChangeLog>

View File

@@ -6,6 +6,7 @@ import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.NoRouteToHostException; import java.net.NoRouteToHostException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
@@ -56,6 +57,7 @@ class HttpGetterTest {
private CommaFeedConfiguration config; private CommaFeedConfiguration config;
private HttpClientFactory provider;
private HttpGetter getter; private HttpGetter getter;
@BeforeEach @BeforeEach
@@ -78,7 +80,8 @@ class HttpGetterTest {
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1)); Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3); 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 @ParameterizedTest
@@ -172,7 +175,7 @@ class HttpGetterTest {
@Test @Test
void dataTimeout() { void dataTimeout() {
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500)); 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")) this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000))); .respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
@@ -183,7 +186,7 @@ class HttpGetterTest {
@Test @Test
void connectTimeout() { void connectTimeout() {
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500)); 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 // try to connect to a non-routable address
// https://stackoverflow.com/a/904609 // https://stackoverflow.com/a/904609
Exception e = Assertions.assertThrows(Exception.class, () -> getter.get("http://10.255.255.1")); Exception e = Assertions.assertThrows(Exception.class, () -> getter.get("http://10.255.255.1"));
@@ -367,44 +370,44 @@ class HttpGetterTest {
@BeforeEach @BeforeEach
void init() { void init() {
Mockito.when(config.httpClient().blockLocalAddresses()).thenReturn(true); 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 @Test
void localhost() { void localhost() {
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://localhost")); Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://localhost"));
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://127.0.0.1")); Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://127.0.0.1"));
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://2130706433")); Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://2130706433"));
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01")); Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01"));
} }
@Test @Test
void zero() { 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 @Test
void linkLocal() { void linkLocal() {
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://169.254.12.34")); Assertions.assertThrows(UnknownHostException.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.169.254"));
} }
@Test @Test
void multicast() { void multicast() {
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://224.2.3.4")); Assertions.assertThrows(UnknownHostException.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://239.255.255.254"));
} }
@Test @Test
void privateIpv4Ranges() { void privateIpv4Ranges() {
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://10.0.0.1")); Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://10.0.0.1"));
Assertions.assertThrows(HttpGetter.HostNotAllowedException.class, () -> getter.get("http://172.16.0.1")); Assertions.assertThrows(UnknownHostException.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://192.168.0.1"));
} }
@Test @Test
void privateIpv6Ranges() { 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]"));
} }
} }

View File

@@ -1,165 +0,0 @@
package com.commafeed.backend.feed;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.Callable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.feed.parser.FeedParserResult.Content;
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
import com.commafeed.backend.service.NotificationService;
import com.commafeed.frontend.ws.WebSocketSessions;
@ExtendWith(MockitoExtension.class)
class FeedRefreshUpdaterTest {
@Mock
private UnitOfWork unitOfWork;
@Mock
private FeedService feedService;
@Mock
private FeedEntryService feedEntryService;
@Mock
private FeedSubscriptionDAO feedSubscriptionDAO;
@Mock
private UserSettingsDAO userSettingsDAO;
@Mock
private WebSocketSessions webSocketSessions;
@Mock
private NotificationService notificationService;
private FeedRefreshUpdater updater;
private Feed feed;
private User user;
private FeedSubscription subscription;
private Entry entry;
private FeedEntry feedEntry;
@SuppressWarnings("unchecked")
@BeforeEach
void setUp() throws Exception {
MetricRegistry metrics = new MetricRegistry();
updater = new FeedRefreshUpdater(unitOfWork, feedService, feedEntryService, metrics, feedSubscriptionDAO, userSettingsDAO,
webSocketSessions, notificationService);
// UnitOfWork passthrough: execute callables and runnables directly
Mockito.when(unitOfWork.call(Mockito.any())).thenAnswer(inv -> inv.getArgument(0, Callable.class).call());
Mockito.doAnswer(inv -> {
inv.getArgument(0, Runnable.class).run();
return null;
}).when(unitOfWork).run(Mockito.any());
user = new User();
user.setId(1L);
feed = new Feed();
feed.setId(1L);
feed.setUrl("https://example.com/feed.xml");
subscription = new FeedSubscription();
subscription.setId(1L);
subscription.setTitle("My Feed");
subscription.setUser(user);
subscription.setNotifyOnNewEntries(true);
Content content = new Content("Article Title", "content", "author", null, null, null);
entry = new Entry("guid-1", "https://example.com/article", Instant.now(), content);
feedEntry = new FeedEntry();
feedEntry.setUrl("https://example.com/article");
}
@Test
void updateSendsNotificationsForNewEntries() {
Mockito.when(feedSubscriptionDAO.findByFeed(feed)).thenReturn(List.of(subscription));
Mockito.when(feedEntryService.find(feed, entry)).thenReturn(null);
Mockito.when(feedEntryService.create(feed, entry)).thenReturn(feedEntry);
Mockito.when(feedEntryService.applyFilter(subscription, feedEntry)).thenReturn(true);
UserSettings settings = new UserSettings();
settings.setNotificationEnabled(true);
Mockito.when(userSettingsDAO.findByUser(user)).thenReturn(settings);
updater.update(feed, List.of(entry));
Mockito.verify(notificationService).notify(settings, subscription, feedEntry);
}
@Test
void updateDoesNotNotifyWhenSubscriptionNotifyDisabled() {
subscription.setNotifyOnNewEntries(false);
Mockito.when(feedSubscriptionDAO.findByFeed(feed)).thenReturn(List.of(subscription));
Mockito.when(feedEntryService.find(feed, entry)).thenReturn(null);
Mockito.when(feedEntryService.create(feed, entry)).thenReturn(feedEntry);
Mockito.when(feedEntryService.applyFilter(subscription, feedEntry)).thenReturn(true);
updater.update(feed, List.of(entry));
Mockito.verify(notificationService, Mockito.never()).notify(Mockito.any(), Mockito.any(), Mockito.any());
}
@Test
void updateDoesNotNotifyWhenUserNotificationsDisabled() {
Mockito.when(feedSubscriptionDAO.findByFeed(feed)).thenReturn(List.of(subscription));
Mockito.when(feedEntryService.find(feed, entry)).thenReturn(null);
Mockito.when(feedEntryService.create(feed, entry)).thenReturn(feedEntry);
Mockito.when(feedEntryService.applyFilter(subscription, feedEntry)).thenReturn(true);
UserSettings settings = new UserSettings();
settings.setNotificationEnabled(false);
Mockito.when(userSettingsDAO.findByUser(user)).thenReturn(settings);
updater.update(feed, List.of(entry));
Mockito.verify(notificationService, Mockito.never()).notify(Mockito.any(), Mockito.any(), Mockito.any());
}
@Test
void updateDoesNotNotifyWhenNoUserSettings() {
Mockito.when(feedSubscriptionDAO.findByFeed(feed)).thenReturn(List.of(subscription));
Mockito.when(feedEntryService.find(feed, entry)).thenReturn(null);
Mockito.when(feedEntryService.create(feed, entry)).thenReturn(feedEntry);
Mockito.when(feedEntryService.applyFilter(subscription, feedEntry)).thenReturn(true);
Mockito.when(userSettingsDAO.findByUser(user)).thenReturn(null);
updater.update(feed, List.of(entry));
Mockito.verify(notificationService, Mockito.never()).notify(Mockito.any(), Mockito.any(), Mockito.any());
}
@Test
void updateDoesNotNotifyForExistingEntries() {
Mockito.when(feedSubscriptionDAO.findByFeed(feed)).thenReturn(List.of(subscription));
Mockito.when(feedEntryService.find(feed, entry)).thenReturn(feedEntry);
updater.update(feed, List.of(entry));
Mockito.verify(notificationService, Mockito.never()).notify(Mockito.any(), Mockito.any(), Mockito.any());
}
}

View File

@@ -1,234 +0,0 @@
package com.commafeed.backend.service;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
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.NotificationType;
@SuppressWarnings("unchecked")
@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {
@Mock
private HttpClient httpClient;
@Mock
private HttpResponse<String> httpResponse;
private NotificationService notificationService;
@BeforeEach
void setUp() {
notificationService = new NotificationService(httpClient);
}
private void stubHttpClient() throws Exception {
Mockito.when(httpResponse.statusCode()).thenReturn(200);
Mockito.when(httpClient.send(Mockito.any(HttpRequest.class), Mockito.<BodyHandler<String>> any())).thenReturn(httpResponse);
}
private HttpRequest captureRequest() throws Exception {
ArgumentCaptor<HttpRequest> captor = ArgumentCaptor.forClass(HttpRequest.class);
Mockito.verify(httpClient).send(captor.capture(), Mockito.<BodyHandler<String>> any());
return captor.getValue();
}
@Test
void sendNtfyBuildsCorrectRequest() throws Exception {
stubHttpClient();
UserSettings settings = newSettings(NotificationType.NTFY);
settings.setNotificationServerUrl("https://ntfy.example.com");
settings.setNotificationTopic("my-topic");
settings.setNotificationToken("my-token");
FeedSubscription sub = newSubscription("My Feed");
FeedEntry entry = newEntry("New Article", "https://example.com/article");
notificationService.notify(settings, sub, entry);
HttpRequest request = captureRequest();
Assertions.assertEquals("https://ntfy.example.com/my-topic", request.uri().toString());
Assertions.assertEquals("My Feed: New Article", request.headers().firstValue("Title").orElse(null));
Assertions.assertEquals("https://example.com/article", request.headers().firstValue("Click").orElse(null));
Assertions.assertEquals("Bearer my-token", request.headers().firstValue("Authorization").orElse(null));
}
@Test
void sendNtfyOmitsOptionalHeaders() throws Exception {
stubHttpClient();
UserSettings settings = newSettings(NotificationType.NTFY);
settings.setNotificationServerUrl("https://ntfy.example.com");
settings.setNotificationTopic("my-topic");
FeedSubscription sub = newSubscription("My Feed");
FeedEntry entry = newEntry("Title", "");
notificationService.notify(settings, sub, entry);
HttpRequest request = captureRequest();
Assertions.assertTrue(request.headers().firstValue("Click").isEmpty());
Assertions.assertTrue(request.headers().firstValue("Authorization").isEmpty());
}
@Test
void sendNtfySkipsWhenMissingConfig() throws Exception {
UserSettings settings = newSettings(NotificationType.NTFY);
settings.setNotificationTopic("topic");
notificationService.notify(settings, newSubscription("F"), newEntry("T", "U"));
Mockito.verify(httpClient, Mockito.never()).send(Mockito.any(), Mockito.any());
UserSettings settings2 = newSettings(NotificationType.NTFY);
settings2.setNotificationServerUrl("https://ntfy.example.com");
notificationService.notify(settings2, newSubscription("F"), newEntry("T", "U"));
Mockito.verify(httpClient, Mockito.never()).send(Mockito.any(), Mockito.any());
}
@Test
void sendGotifyBuildsCorrectRequest() throws Exception {
stubHttpClient();
UserSettings settings = newSettings(NotificationType.GOTIFY);
settings.setNotificationServerUrl("https://gotify.example.com/");
settings.setNotificationToken("app-token");
FeedSubscription sub = newSubscription("My Feed");
FeedEntry entry = newEntry("New Article", "https://example.com/article");
notificationService.notify(settings, sub, entry);
HttpRequest request = captureRequest();
Assertions.assertEquals("https://gotify.example.com/message", request.uri().toString());
Assertions.assertEquals("app-token", request.headers().firstValue("X-Gotify-Key").orElse(null));
Assertions.assertEquals("application/json", request.headers().firstValue("Content-Type").orElse(null));
}
@Test
void sendGotifySkipsWhenMissingConfig() throws Exception {
UserSettings settings = newSettings(NotificationType.GOTIFY);
settings.setNotificationToken("token");
notificationService.notify(settings, newSubscription("F"), newEntry("T", "U"));
Mockito.verify(httpClient, Mockito.never()).send(Mockito.any(), Mockito.any());
UserSettings settings2 = newSettings(NotificationType.GOTIFY);
settings2.setNotificationServerUrl("https://gotify.example.com");
notificationService.notify(settings2, newSubscription("F"), newEntry("T", "U"));
Mockito.verify(httpClient, Mockito.never()).send(Mockito.any(), Mockito.any());
}
@Test
void sendPushoverBuildsCorrectRequest() throws Exception {
stubHttpClient();
UserSettings settings = newSettings(NotificationType.PUSHOVER);
settings.setNotificationToken("po-token");
settings.setNotificationUserKey("po-user");
FeedSubscription sub = newSubscription("My Feed");
FeedEntry entry = newEntry("New Article", "https://example.com/article");
notificationService.notify(settings, sub, entry);
HttpRequest request = captureRequest();
Assertions.assertEquals("https://api.pushover.net/1/messages.json", request.uri().toString());
Assertions.assertEquals("application/x-www-form-urlencoded", request.headers().firstValue("Content-Type").orElse(null));
}
@Test
void sendPushoverOmitsUrlWhenBlank() throws Exception {
stubHttpClient();
UserSettings settings = newSettings(NotificationType.PUSHOVER);
settings.setNotificationToken("po-token");
settings.setNotificationUserKey("po-user");
FeedSubscription sub = newSubscription("My Feed");
FeedEntry entry = newEntry("Title", "");
notificationService.notify(settings, sub, entry);
Mockito.verify(httpClient).send(Mockito.any(HttpRequest.class), Mockito.<BodyHandler<String>> any());
}
@Test
void sendPushoverSkipsWhenMissingConfig() throws Exception {
UserSettings settings = newSettings(NotificationType.PUSHOVER);
settings.setNotificationUserKey("user");
notificationService.notify(settings, newSubscription("F"), newEntry("T", "U"));
Mockito.verify(httpClient, Mockito.never()).send(Mockito.any(), Mockito.any());
UserSettings settings2 = newSettings(NotificationType.PUSHOVER);
settings2.setNotificationToken("token");
notificationService.notify(settings2, newSubscription("F"), newEntry("T", "U"));
Mockito.verify(httpClient, Mockito.never()).send(Mockito.any(), Mockito.any());
}
@Test
void notifyDoesNotPropagateExceptions() throws Exception {
Mockito.when(httpClient.send(Mockito.any(HttpRequest.class), Mockito.<BodyHandler<String>> any()))
.thenThrow(new IOException("connection failed"));
UserSettings settings = newSettings(NotificationType.NTFY);
settings.setNotificationServerUrl("https://ntfy.example.com");
settings.setNotificationTopic("topic");
Assertions.assertDoesNotThrow(() -> notificationService.notify(settings, newSubscription("Feed"), newEntry("Title", "url")));
}
@Test
void notifyUsesNewEntryAsFallbackTitle() throws Exception {
stubHttpClient();
UserSettings settings = newSettings(NotificationType.NTFY);
settings.setNotificationServerUrl("https://ntfy.example.com");
settings.setNotificationTopic("topic");
FeedSubscription sub = newSubscription("Feed");
FeedEntry entryNoContent = new FeedEntry();
entryNoContent.setUrl("https://example.com");
notificationService.notify(settings, sub, entryNoContent);
HttpRequest request = captureRequest();
Assertions.assertEquals("Feed: New entry", request.headers().firstValue("Title").orElse(null));
}
private UserSettings newSettings(NotificationType type) {
UserSettings settings = new UserSettings();
settings.setNotificationEnabled(true);
settings.setNotificationType(type);
return settings;
}
private FeedSubscription newSubscription(String title) {
FeedSubscription sub = new FeedSubscription();
sub.setTitle(title);
return sub;
}
private FeedEntry newEntry(String title, String url) {
FeedEntryContent content = new FeedEntryContent();
content.setTitle(title);
FeedEntry entry = new FeedEntry();
entry.setContent(content);
entry.setUrl(url);
return entry;
}
}

View File

@@ -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;
}
}

View File

@@ -39,6 +39,7 @@ public abstract class BaseIT {
private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/"); private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/");
@Getter
private MockServerClient mockServerClient; private MockServerClient mockServerClient;
private Client client; private Client client;
private String feedUrl; private String feedUrl;
@@ -122,10 +123,15 @@ public abstract class BaseIT {
} }
protected Long subscribe(String feedUrl, String categoryId) { 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 subscribeRequest = new SubscribeRequest();
subscribeRequest.setUrl(feedUrl); subscribeRequest.setUrl(feedUrl);
subscribeRequest.setTitle("my title for this feed"); subscribeRequest.setTitle("my title for this feed");
subscribeRequest.setCategoryId(categoryId); subscribeRequest.setCategoryId(categoryId);
subscribeRequest.setPushNotificationsEnabled(pushNotificationsEnabled);
return RestAssured.given() return RestAssured.given()
.body(subscribeRequest) .body(subscribeRequest)
.contentType(ContentType.JSON) .contentType(ContentType.JSON)

View File

@@ -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)));
}
}

View File

@@ -23,6 +23,7 @@ class ServerIT extends BaseIT {
Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval()); Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval());
Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration()); Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration());
Assertions.assertEquals(4, serverInfos.getMinimumPasswordLength()); Assertions.assertEquals(4, serverInfos.getMinimumPasswordLength());
Assertions.assertTrue(serverInfos.isPushNotificationsEnabled());
} }
} }