Merge branch 'lpoirothattermann-master'

This commit is contained in:
Athou
2026-02-18 20:53:50 +01:00
69 changed files with 2293 additions and 196 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

@@ -6298,7 +6298,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",

View File

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

View File

@@ -29,6 +29,7 @@ export interface Subscription {
newestItemTime?: number newestItemTime?: number
filter?: string filter?: string
filterLegacy?: string filterLegacy?: string
pushNotificationsEnabled: boolean
} }
export interface Category { export interface Category {
@@ -110,6 +111,7 @@ export interface FeedModificationRequest {
categoryId?: string categoryId?: string
position?: number position?: number
filter?: string filter?: string
pushNotificationsEnabled: boolean
} }
export interface GetEntriesRequest { export interface GetEntriesRequest {
@@ -236,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 {
@@ -249,6 +252,16 @@ export interface SharingSettings {
buffer: boolean buffer: boolean
} }
export type PushNotificationType = "ntfy" | "gotify" | "pushover"
export interface PushNotificationSettings {
type?: PushNotificationType
serverUrl?: string
userId?: string
userSecret?: string
topic?: string
}
export interface Settings { export interface Settings {
language?: string language?: string
readingMode: ReadingMode readingMode: ReadingMode
@@ -271,6 +284,7 @@ export interface Settings {
disablePullToRefresh: boolean disablePullToRefresh: boolean
primaryColor?: string primaryColor?: string
sharingSettings: SharingSettings sharingSettings: SharingSettings
pushNotificationSettings: PushNotificationSettings
} }
export interface LocalSettings { export interface LocalSettings {
@@ -290,6 +304,7 @@ export interface SubscribeRequest {
url: string url: string
title: string title: string
categoryId?: string categoryId?: string
pushNotificationsEnabled: boolean
} }
export interface TagRequest { export interface TagRequest {

View File

@@ -11,6 +11,7 @@ import {
changeMarkAllAsReadConfirmation, changeMarkAllAsReadConfirmation,
changeMarkAllAsReadNavigateToUnread, changeMarkAllAsReadNavigateToUnread,
changeMobileFooter, changeMobileFooter,
changeNotificationSettings,
changePrimaryColor, changePrimaryColor,
changeReadingMode, changeReadingMode,
changeReadingOrder, changeReadingOrder,
@@ -148,6 +149,10 @@ export const userSlice = createSlice({
if (!state.settings) return if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
}) })
builder.addCase(changeNotificationSettings.pending, (state, action) => {
if (!state.settings) return
state.settings.pushNotificationSettings = action.meta.arg
})
builder.addMatcher( builder.addMatcher(
isAnyOf( isAnyOf(
@@ -167,7 +172,8 @@ export const userSlice = createSlice({
changeUnreadCountFavicon.fulfilled, changeUnreadCountFavicon.fulfilled,
changeDisablePullToRefresh.fulfilled, changeDisablePullToRefresh.fulfilled,
changePrimaryColor.fulfilled, changePrimaryColor.fulfilled,
changeSharingSetting.fulfilled changeSharingSetting.fulfilled,
changeNotificationSettings.fulfilled
), ),
() => { () => {
showNotification({ showNotification({

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, 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))
@@ -157,3 +157,15 @@ export const changeSharingSetting = createAppAsyncThunk(
}) })
} }
) )
export const changeNotificationSettings = createAppAsyncThunk(
"settings/notificationSettings",
(pushNotificationSettings: PushNotificationSettings, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
pushNotificationSettings,
})
}
)

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

@@ -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,6 +29,7 @@ export function Subscribe() {
url: "", url: "",
title: "", title: "",
categoryId: Constants.categories.all.id, categoryId: Constants.categories.all.id,
pushNotificationsEnabled: false,
}, },
}) })
@@ -103,6 +105,9 @@ 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 />
<ReceivePushNotificationsChechbox
{...step1Form.getInputProps("pushNotificationsEnabled", { type: "checkbox" })}
/>
</Stack> </Stack>
</Stepper.Step> </Stepper.Step>
</Stepper> </Stepper>

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,6 +34,10 @@ msgstr "<0> هل تحتاج إلى حساب؟ </0> <1> اشترك! </ 1>"
msgid "About" msgid "About"
msgstr "حول" msgstr "حول"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "الإجراءات" msgstr "الإجراءات"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "مفتاح API" msgstr "مفتاح API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "هل أنت متأكد أنك تريد حذف الفئة <0> {categoryName} </0>؟" msgstr "هل أنت متأكد أنك تريد حذف الفئة <0> {categoryName} </0>؟"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "الملف الشخصي" msgstr "الملف الشخصي"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "استعادة كلمة السر" msgstr "استعادة كلمة السر"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "ضع التركيز على الإدخال التالي دون فتحه" msgstr "ضع التركيز على الإدخال التالي دون فتحه"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي" msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"
@@ -1073,6 +1117,10 @@ msgstr "إلغاء النجم"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "إلغاء الاشتراك" msgstr "إلغاء الاشتراك"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "اسم المستخدم" msgstr "اسم المستخدم"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Necessites un compte?</0><1>Registreu-vos!</1>"
msgid "About" msgid "About"
msgstr "Sobre" msgstr "Sobre"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Accions" msgstr "Accions"
@@ -94,6 +98,14 @@ msgstr "Anunci"
msgid "API key" msgid "API key"
msgstr "Clau API" msgstr "Clau API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Esteu segur que voleu suprimir la categoria <0>{categoryName}</0>?" msgstr "Esteu segur que voleu suprimir la categoria <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr "Color primari"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Recuperar la contrasenya" msgstr "Recuperar la contrasenya"
@@ -841,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"
@@ -873,6 +908,11 @@ 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/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Posa el focus a la següent entrada sense obrir-la" msgstr "Posa el focus a la següent entrada sense obrir-la"
@@ -1050,6 +1090,10 @@ msgstr "Canvia la barra lateral"
msgid "Toggle starred status of current entry" 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/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo" msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Desestrellar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Donar-se de baixa" msgstr "Donar-se de baixa"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nom d'usuari" msgstr "Nom d'usuari"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Potřebujete účet?</0><1>Zaregistrujte se!</1>"
msgid "About" msgid "About"
msgstr "Asi" msgstr "Asi"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Akce" msgstr "Akce"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Klíč API" msgstr "Klíč API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Opravdu chcete smazat kategorii <0>{categoryName}</0>?" msgstr "Opravdu chcete smazat kategorii <0>{categoryName}</0>?"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Obnovte heslo" msgstr "Obnovte heslo"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Zaměřte se na další položku, aniž byste ji otevřeli" msgstr "Zaměřte se na další položku, aniž byste ji otevřeli"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo" msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Odstranit hvězdu"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Odhlásit odběr" msgstr "Odhlásit odběr"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Uživatelské jméno" msgstr "Uživatelské jméno"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Angen cyfrif?</0><1>Ymunwch!</1>"
msgid "About" msgid "About"
msgstr "Ynghylch" msgstr "Ynghylch"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Camau gweithredu" msgstr "Camau gweithredu"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Allwedd API" msgstr "Allwedd API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Ydych chi'n siŵr eich bod am ddileu categori <0>{categoryName}</0>?" msgstr "Ydych chi'n siŵr eich bod am ddileu categori <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Proffil" msgstr "Proffil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Adfer cyfrinair" msgstr "Adfer cyfrinair"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Gosodwch ffocws ar y cofnod nesaf heb ei agor" msgstr "Gosodwch ffocws ar y cofnod nesaf heb ei agor"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo" msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"
@@ -1073,6 +1117,10 @@ msgstr "dad-seren"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Dad-danysgrifio" msgstr "Dad-danysgrifio"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Enw defnyddiwr" msgstr "Enw defnyddiwr"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Har du brug for en konto?</0><1>Tilmeld dig!</1>"
msgid "About" msgid "About"
msgstr "Omkring" msgstr "Omkring"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Handlinger" msgstr "Handlinger"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nøgle" msgstr "API-nøgle"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Er du sikker på, at du vil slette kategori <0>{categoryName}</0>?" msgstr "Er du sikker på, at du vil slette kategori <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Gendan adgangskode" msgstr "Gendan adgangskode"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Sæt fokus på næste post uden at åbne den" msgstr "Sæt fokus på næste post uden at åbne den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Afmeld" msgstr "Afmeld"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Brugernavn" msgstr "Brugernavn"

View File

@@ -34,6 +34,10 @@ 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/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Aktionen" msgstr "Aktionen"
@@ -94,6 +98,14 @@ msgstr "Ankündigung"
msgid "API key" msgid "API key"
msgstr "API-Schlüssel" msgstr "API-Schlüssel"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Sind Sie sicher, dass Sie die Kategorie <0>{categoryName}</0> löschen möchten?" msgstr "Sind Sie sicher, dass Sie die Kategorie <0>{categoryName}</0> löschen möchten?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Kennwort wiederherstellen" msgstr "Kennwort wiederherstellen"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Fokus auf den nächsten Eintrag setzen, ohne ihn zu öffnen" msgstr "Fokus auf den nächsten Eintrag setzen, ohne ihn zu öffnen"
@@ -1050,6 +1090,10 @@ msgstr "Sidebar an- und ausschalten"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Markierungsstatus des aktuellen Eintrags ändern" msgstr "Markierungsstatus des aktuellen Eintrags ändern"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo" msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Stern entfernen"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Abbestellen" msgstr "Abbestellen"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Benutzername" msgstr "Benutzername"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Need an account?</0><1>Sign up!</1>"
msgid "About" msgid "About"
msgstr "About" msgstr "About"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr "Access token"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Actions" msgstr "Actions"
@@ -94,6 +98,14 @@ msgstr "Announcement"
msgid "API key" msgid "API key"
msgstr "API key" msgstr "API key"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr "API token"
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr "App token"
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Are you sure you want to delete category <0>{categoryName}</0>?" msgstr "Are you sure you want to delete category <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr "Build a filter expression to indicate what you want to read. Entries tha
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr "Primary color"
msgid "Profile" msgid "Profile"
msgstr "Profile" msgstr "Profile"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr "Push notification service"
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr "Push notifications"
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr "Push notifications are not configured in your user settings."
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr "Push notifications are not enabled on this CommaFeed instance."
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr "Receive push notifications"
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Recover password" msgstr "Recover password"
@@ -841,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"
@@ -873,6 +908,11 @@ 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/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr "Server URL"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Set focus on next entry without opening it" msgstr "Set focus on next entry without opening it"
@@ -1050,6 +1090,10 @@ msgstr "Toggle sidebar"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Toggle starred status of current entry" msgstr "Toggle starred status of current entry"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr "Topic"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Try out CommaFeed with the demo account: demo/demo" msgstr "Try out CommaFeed with the demo account: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Unstar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Unsubscribe" msgstr "Unsubscribe"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr "User key"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "User name" msgstr "User name"

View File

@@ -35,6 +35,10 @@ msgstr "<0>¿Necesitas una cuenta?</0><1>¡Regístrate!</1>"
msgid "About" msgid "About"
msgstr "Acerca de" msgstr "Acerca de"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Acciones" msgstr "Acciones"
@@ -95,6 +99,14 @@ msgstr "Anuncio"
msgid "API key" msgid "API key"
msgstr "Clave API" msgstr "Clave API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "¿Estás seguro de que deseas eliminar la categoría <0>{categoryName}</0>?" msgstr "¿Estás seguro de que deseas eliminar la categoría <0>{categoryName}</0>?"
@@ -159,6 +171,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -808,6 +821,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Recuperar contraseña" msgstr "Recuperar contraseña"
@@ -842,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"
@@ -874,6 +909,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Establecer el foco en la siguiente entrada sin abrirla" msgstr "Establecer el foco en la siguiente entrada sin abrirla"
@@ -1051,6 +1091,10 @@ msgstr "Alternar barra lateral"
msgid "Toggle starred status of current entry" 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/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prueba CommaFeed con la cuenta de demostración: demo/demo" msgstr "Prueba CommaFeed con la cuenta de demostración: demo/demo"
@@ -1074,6 +1118,10 @@ msgstr "Desmarcar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Cancelar suscripción" msgstr "Cancelar suscripción"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nombre de usuario" msgstr "Nombre de usuario"

View File

@@ -34,6 +34,10 @@ msgstr "<0>به یک حساب نیاز دارید؟</0><1>ثبت نام کنید
msgid "About" msgid "About"
msgstr "در مورد" msgstr "در مورد"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "اعمال" msgstr "اعمال"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "کلید API" msgstr "کلید API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "آیا مطمئن هستید که می خواهید دسته <0>{categoryName}</0> را حذف کنید؟" msgstr "آیا مطمئن هستید که می خواهید دسته <0>{categoryName}</0> را حذف کنید؟"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "نمایه" msgstr "نمایه"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "بازیابی رمز عبور" msgstr "بازیابی رمز عبور"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "فوکوس را روی ورودی بعدی بدون باز کردن آن تنظیم کنید" msgstr "فوکوس را روی ورودی بعدی بدون باز کردن آن تنظیم کنید"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو" msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "لغو اشتراک" msgstr "لغو اشتراک"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "نام کاربری" msgstr "نام کاربری"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Tarvitsetko tilin?</0><1>Rekisteröidy!</1>"
msgid "About" msgid "About"
msgstr "Noin" msgstr "Noin"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Toimet" msgstr "Toimet"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-avain" msgstr "API-avain"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Haluatko varmasti poistaa luokan <0>{categoryName}</0>?" msgstr "Haluatko varmasti poistaa luokan <0>{categoryName}</0>?"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profiili" msgstr "Profiili"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Palauta salasana" msgstr "Palauta salasana"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Keskitä seuraavaan merkintään avaamatta sitä" msgstr "Keskitä seuraavaan merkintään avaamatta sitä"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo" msgstr "Kokeile CommaFeediä demotilillä: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Poista tähti"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Peruuta tilaus" msgstr "Peruuta tilaus"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Käyttäjänimi" msgstr "Käyttäjänimi"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Besoin d'un compte ?</0><1>Enregistrez-vous !</1>"
msgid "About" msgid "About"
msgstr "À propos" msgstr "À propos"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Actions" msgstr "Actions"
@@ -94,6 +98,14 @@ msgstr "Annonces"
msgid "API key" msgid "API key"
msgstr "Clé API" msgstr "Clé API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Êtes-vous sûr de vouloir supprimer la catégorie <0>{categoryName}</0> ?" msgstr "Êtes-vous sûr de vouloir supprimer la catégorie <0>{categoryName}</0> ?"
@@ -158,6 +170,7 @@ msgstr "Créez une expression régulière pour filtrer ce que vous voulez lire.
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr "Couleur d'ambiance"
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Récupérer le mot de passe" msgstr "Récupérer le mot de passe"
@@ -841,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"
@@ -873,6 +908,11 @@ 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/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Sélectionner l'article suivant sans l'ouvrir" msgstr "Sélectionner l'article suivant sans l'ouvrir"
@@ -1050,6 +1090,10 @@ msgstr "Montrer/cacher la barre latérale"
msgid "Toggle starred status of current entry" 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/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo" msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Retirer des favoris"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Se désabonner" msgstr "Se désabonner"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nom" msgstr "Nom"

View File

@@ -35,6 +35,10 @@ msgstr "<0>Necesitas unha conta?</0><1>Crea unha!</1>"
msgid "About" msgid "About"
msgstr "Sobre" msgstr "Sobre"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Accións" msgstr "Accións"
@@ -95,6 +99,14 @@ msgstr "Anuncio"
msgid "API key" msgid "API key"
msgstr "Clave API" msgstr "Clave API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Tes certeza de querer eliminar a categoría <0>{categoryName}</0>?" msgstr "Tes certeza de querer eliminar a categoría <0>{categoryName}</0>?"
@@ -159,6 +171,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -808,6 +821,27 @@ msgstr "Cor destacada"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Recuperar o contrasinal" msgstr "Recuperar o contrasinal"
@@ -842,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"
@@ -874,6 +909,11 @@ 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/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Establece o foco no seguinte artigo sen abrilo" msgstr "Establece o foco no seguinte artigo sen abrilo"
@@ -1051,6 +1091,10 @@ msgstr "Activación do panel lateral"
msgid "Toggle starred status of current entry" 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/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo" msgstr "Proba CommaFeed coa conta de demostración: demo/demo"
@@ -1074,6 +1118,10 @@ msgstr "Retirar estrela"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Cancelar a subscrición" msgstr "Cancelar a subscrición"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Identificador" msgstr "Identificador"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Fiókra van szüksége?</0><1>Regisztráljon!</1>"
msgid "About" msgid "About"
msgstr "Kb" msgstr "Kb"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Műveletek" msgstr "Műveletek"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API kulcs" msgstr "API kulcs"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Biztosan törölni szeretné a(z) <0>{categoryName}</0> kategóriát?" msgstr "Biztosan törölni szeretné a(z) <0>{categoryName}</0> kategóriát?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Jelszó helyreállítása" msgstr "Jelszó helyreállítása"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Állítsa a fókuszt a következő bejegyzésre anélkül, hogy megnyitná azt" msgstr "Állítsa a fókuszt a következő bejegyzésre anélkül, hogy megnyitná azt"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo" msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Leiratkozás" msgstr "Leiratkozás"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Felhasználónév" msgstr "Felhasználónév"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Butuh akun?</0><1>Daftar!</1>"
msgid "About" msgid "About"
msgstr "Tentang" msgstr "Tentang"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Tindakan" msgstr "Tindakan"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "kunci API" msgstr "kunci API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Anda yakin ingin menghapus kategori <0>{categoryName}</0>?" msgstr "Anda yakin ingin menghapus kategori <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Pulihkan kata sandi" msgstr "Pulihkan kata sandi"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Tetapkan fokus pada entri berikutnya tanpa membukanya" msgstr "Tetapkan fokus pada entri berikutnya tanpa membukanya"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo" msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Hapus bintang"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Berhenti berlangganan" msgstr "Berhenti berlangganan"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nama pengguna" msgstr "Nama pengguna"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Hai bisogno di un account?</0><1>Registrati!</1>"
msgid "About" msgid "About"
msgstr "Circa" msgstr "Circa"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Azioni" msgstr "Azioni"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Chiave API" msgstr "Chiave API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Sei sicuro di voler eliminare la categoria <0>{categoryName}</0>?" msgstr "Sei sicuro di voler eliminare la categoria <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profilo" msgstr "Profilo"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Recupera password" msgstr "Recupera password"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Imposta il focus sulla voce successiva senza aprirla" msgstr "Imposta il focus sulla voce successiva senza aprirla"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed con il conto demo: demo/demo" msgstr "Prova CommaFeed con il conto demo: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Elimina le stelle"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Annulla iscrizione" msgstr "Annulla iscrizione"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nome utente" msgstr "Nome utente"

View File

@@ -34,6 +34,10 @@ msgstr "<0>アカウントが必要ですか?</0><1>サインアップ!</1>"
msgid "About" msgid "About"
msgstr "About" msgstr "About"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "アクション" msgstr "アクション"
@@ -94,6 +98,14 @@ msgstr "お知らせ"
msgid "API key" msgid "API key"
msgstr "APIキー" msgstr "APIキー"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "カテゴリ <0>{categoryName}</0> を削除してもよろしいですか?" msgstr "カテゴリ <0>{categoryName}</0> を削除してもよろしいですか?"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "プロフィール" msgstr "プロフィール"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "パスワードの回復" msgstr "パスワードの回復"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "次のエントリーを開かずにフォーカスする" msgstr "次のエントリーを開かずにフォーカスする"
@@ -1050,6 +1090,10 @@ msgstr "サイドバーを切り替える"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "現在のエントリーのスターステータスを切り替える" msgstr "現在のエントリーのスターステータスを切り替える"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "デモアカウントでCommaFeedを試す: demo/demo" msgstr "デモアカウントでCommaFeedを試す: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "スターを外す"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "退会" msgstr "退会"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "ユーザー名" msgstr "ユーザー名"

View File

@@ -34,6 +34,10 @@ msgstr "<0>계정이 필요하십니까?</0><1>가입하세요!</1>"
msgid "About" msgid "About"
msgstr "정보" msgstr "정보"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "액션" msgstr "액션"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API 키" msgstr "API 키"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "<0>{categoryName}</0> 카테고리를 삭제하시겠습니까?" msgstr "<0>{categoryName}</0> 카테고리를 삭제하시겠습니까?"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "프로필" msgstr "프로필"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "비밀번호 복구" msgstr "비밀번호 복구"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "열지 않고 다음 항목에 포커스 설정" msgstr "열지 않고 다음 항목에 포커스 설정"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo" msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "별표 제거"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "구독 취소" msgstr "구독 취소"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "사용자 이름" msgstr "사용자 이름"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Perlukan akaun?</0><1>Daftar!</1>"
msgid "About" msgid "About"
msgstr "Mengenai" msgstr "Mengenai"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Tindakan" msgstr "Tindakan"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Kunci API" msgstr "Kunci API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Adakah anda pasti mahu memadamkan kategori <0>{categoryName}</0>?" msgstr "Adakah anda pasti mahu memadamkan kategori <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Pulihkan kata laluan" msgstr "Pulihkan kata laluan"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Tetapkan fokus pada entri seterusnya tanpa membukanya" msgstr "Tetapkan fokus pada entri seterusnya tanpa membukanya"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo" msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Nyahbintang"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Nyahlanggan" msgstr "Nyahlanggan"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nama pengguna" msgstr "Nama pengguna"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
msgid "About" msgid "About"
msgstr "Omtrent" msgstr "Omtrent"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Handlinger" msgstr "Handlinger"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nøkkel" msgstr "API-nøkkel"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Er du sikker på at du vil slette kategori <0>{categoryName}</0>?" msgstr "Er du sikker på at du vil slette kategori <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Gjenopprett passord" msgstr "Gjenopprett passord"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Sett fokus på neste oppføring uten å åpne den" msgstr "Sett fokus på neste oppføring uten å åpne den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Fjern stjerne"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Avslutt abonnementet" msgstr "Avslutt abonnementet"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Brukernavn" msgstr "Brukernavn"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Een account nodig?</0><1>Meld je aan!</1>"
msgid "About" msgid "About"
msgstr "Over" msgstr "Over"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Acties" msgstr "Acties"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-sleutel" msgstr "API-sleutel"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Weet je zeker dat je categorie <0>{categoryName}</0> wilt verwijderen?" msgstr "Weet je zeker dat je categorie <0>{categoryName}</0> wilt verwijderen?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profiel" msgstr "Profiel"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "wachtwoord herstellen" msgstr "wachtwoord herstellen"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Stel de focus in op het volgende item zonder het te openen" msgstr "Stel de focus in op het volgende item zonder het te openen"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo" msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Sterren uit"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Afmelden" msgstr "Afmelden"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Gebruikersnaam" msgstr "Gebruikersnaam"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
msgid "About" msgid "About"
msgstr "Omtrent" msgstr "Omtrent"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Handlinger" msgstr "Handlinger"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nøkkel" msgstr "API-nøkkel"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Er du sikker på at du vil slette kategori <0>{categoryName}</0>?" msgstr "Er du sikker på at du vil slette kategori <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Gjenopprett passord" msgstr "Gjenopprett passord"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Sett fokus på neste oppføring uten å åpne den" msgstr "Sett fokus på neste oppføring uten å åpne den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Fjern stjerne"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Avslutt abonnementet" msgstr "Avslutt abonnementet"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Brukernavn" msgstr "Brukernavn"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Potrzebujesz konta?</0><1>Zarejestruj się!</1>"
msgid "About" msgid "About"
msgstr "O" msgstr "O"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Akcje" msgstr "Akcje"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "klucz API" msgstr "klucz API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Czy na pewno chcesz usunąć kategorię <0>{categoryName}</0>?" msgstr "Czy na pewno chcesz usunąć kategorię <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Odzyskaj hasło" msgstr "Odzyskaj hasło"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Ustaw fokus na następnym wpisie bez otwierania go" msgstr "Ustaw fokus na następnym wpisie bez otwierania go"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo" msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Anuluj subskrypcję" msgstr "Anuluj subskrypcję"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nazwa użytkownika" msgstr "Nazwa użytkownika"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Precisa de uma conta?</0><1>Inscreva-se!</1>"
msgid "About" msgid "About"
msgstr "Sobre" msgstr "Sobre"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Ações" msgstr "Ações"
@@ -94,6 +98,14 @@ msgstr "Aviso"
msgid "API key" msgid "API key"
msgstr "chave de API" msgstr "chave de API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Tem certeza de que deseja excluir a categoria <0>{categoryName}</0>?" msgstr "Tem certeza de que deseja excluir a categoria <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr "Cor primária"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Recuperar senha" msgstr "Recuperar senha"
@@ -841,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"
@@ -873,6 +908,11 @@ 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/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Definir o foco na próxima entrada sem abri-la" msgstr "Definir o foco na próxima entrada sem abri-la"
@@ -1050,6 +1090,10 @@ msgstr "Alternar barra lateral"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Alternar estrela da entrada atual" msgstr "Alternar estrela da entrada atual"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Experimente o CommaFeed com a conta demo: demo/demo" msgstr "Experimente o CommaFeed com a conta demo: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Desestrelar"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Cancelar inscrição" msgstr "Cancelar inscrição"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Nome de usuário" msgstr "Nome de usuário"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Нужен аккаунт?</0><1>Зарегистрируйтесь!<
msgid "About" msgid "About"
msgstr "О CommaFeed" msgstr "О CommaFeed"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Действия" msgstr "Действия"
@@ -94,6 +98,14 @@ msgstr "Объявление"
msgid "API key" msgid "API key"
msgstr "Ключ API" msgstr "Ключ API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Вы уверены, что хотите удалить категорию <0>{categoryName}</0>?" msgstr "Вы уверены, что хотите удалить категорию <0>{categoryName}</0>?"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Профиль" msgstr "Профиль"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Восстановить пароль" msgstr "Восстановить пароль"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Установить фокус на следующую запись, не открывая ее." msgstr "Установить фокус на следующую запись, не открывая ее."
@@ -1050,6 +1090,10 @@ msgstr "Переключить боковую панель"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "Переключение статуса избранное для текущей записи" msgstr "Переключение статуса избранное для текущей записи"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Попробуйте CommaFeed на демо аккаунте: demo/demo" msgstr "Попробуйте CommaFeed на демо аккаунте: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Удалить из избранного"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Отписаться" msgstr "Отписаться"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Имя пользователя" msgstr "Имя пользователя"

View File

@@ -34,6 +34,10 @@ msgstr "<0>Potrebujete účet?</0><1>Zaregistrujte sa!</1>"
msgid "About" msgid "About"
msgstr "Asi" msgstr "Asi"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Akcie" msgstr "Akcie"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "Kľúč API" msgstr "Kľúč API"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Naozaj chcete odstrániť kategóriu <0>{categoryName}</0>?" msgstr "Naozaj chcete odstrániť kategóriu <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Obnoviť heslo" msgstr "Obnoviť heslo"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Nastavte zameranie na ďalší záznam bez toho, aby ste ho otvorili" msgstr "Nastavte zameranie na ďalší záznam bez toho, aby ste ho otvorili"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo" msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Odobrať hviezdičku"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Zrušte odber" msgstr "Zrušte odber"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Meno používateľa" msgstr "Meno používateľa"

View File

@@ -34,6 +34,10 @@ 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/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Handlingar" msgstr "Handlingar"
@@ -94,6 +98,14 @@ msgstr ""
msgid "API key" msgid "API key"
msgstr "API-nyckel" msgstr "API-nyckel"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Är du säker på att du vill ta bort kategori <0>{categoryName}</0>?" msgstr "Är du säker på att du vill ta bort kategori <0>{categoryName}</0>?"
@@ -158,6 +170,7 @@ msgstr ""
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Återställ lösenord" msgstr "Återställ lösenord"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Sätt fokus på nästa post utan att öppna den" msgstr "Sätt fokus på nästa post utan att öppna den"
@@ -1050,6 +1090,10 @@ msgstr ""
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed med demokontot: demo/demo" msgstr "Prova CommaFeed med demokontot: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Avregistrera" msgstr "Avregistrera"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Användarnamn" msgstr "Användarnamn"

View File

@@ -34,6 +34,10 @@ 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/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "Eylemler" msgstr "Eylemler"
@@ -94,6 +98,14 @@ msgstr "Duyuru"
msgid "API key" msgid "API key"
msgstr "API anahtarı" msgstr "API anahtarı"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "<0>{categoryName}</0> kategorisini silmek istediğinizden emin misiniz?" msgstr "<0>{categoryName}</0> kategorisini silmek istediğinizden emin misiniz?"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "Şifreyi kurtar" msgstr "Şifreyi kurtar"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr ""
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "Odağı açmadan sonraki girişe ayarlayın" msgstr "Odağı açmadan sonraki girişe ayarlayın"
@@ -1050,6 +1090,10 @@ msgstr "Kenar çubuğunu göster/gizle"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "" msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo" msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"
@@ -1073,6 +1117,10 @@ msgstr "Yıldızı kaldır"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Aboneliği iptal et" msgstr "Aboneliği iptal et"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
msgstr "Kullanıcı adı" msgstr "Kullanıcı adı"

View File

@@ -34,6 +34,10 @@ msgstr "<0>需要一个帐户?</0><1>注册!</1>"
msgid "About" msgid "About"
msgstr "关于" msgstr "关于"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Access token"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
msgstr "操作" msgstr "操作"
@@ -94,6 +98,14 @@ msgstr "公告"
msgid "API key" msgid "API key"
msgstr "API 密钥" msgstr "API 密钥"
#: src/components/settings/PushNotificationSettings.tsx
msgid "API token"
msgstr ""
#: src/components/settings/PushNotificationSettings.tsx
msgid "App token"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "您确定要删除类别 <0>{categoryName}</0> 吗?" msgstr "您确定要删除类别 <0>{categoryName}</0> 吗?"
@@ -158,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
@@ -807,6 +820,27 @@ msgstr "主颜色"
msgid "Profile" msgid "Profile"
msgstr "配置文件" msgstr "配置文件"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notification service"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Push notifications"
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Push notifications are not configured in your user settings."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Push notifications are not enabled on this CommaFeed instance."
msgstr ""
#: src/components/ReceivePushNotificationsChechbox.tsx
msgid "Receive push notifications"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Recover password" msgid "Recover password"
msgstr "找回密码" msgstr "找回密码"
@@ -841,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"
@@ -873,6 +908,11 @@ msgstr "选择下一个未读信息流/类别"
msgid "Select previous unread feed/category" msgid "Select previous unread feed/category"
msgstr "选择上一个未读信息流/类别" msgstr "选择上一个未读信息流/类别"
#: src/components/settings/PushNotificationSettings.tsx
#: src/components/settings/PushNotificationSettings.tsx
msgid "Server URL"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Set focus on next entry without opening it" msgid "Set focus on next entry without opening it"
msgstr "将焦点放在下一个条目而不打开它" msgstr "将焦点放在下一个条目而不打开它"
@@ -1050,6 +1090,10 @@ msgstr "切换侧边栏"
msgid "Toggle starred status of current entry" msgid "Toggle starred status of current entry"
msgstr "切换当前条目的星标状态" msgstr "切换当前条目的星标状态"
#: src/components/settings/PushNotificationSettings.tsx
msgid "Topic"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "使用演示帐户试用 CommaFeeddemo/demo" msgstr "使用演示帐户试用 CommaFeeddemo/demo"
@@ -1073,6 +1117,10 @@ msgstr "取消星标"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "取消订阅" msgstr "取消订阅"
#: src/components/settings/PushNotificationSettings.tsx
msgid "User key"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "User name" msgid "User name"
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

@@ -30,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() {
@@ -142,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={

View File

@@ -1,9 +1,10 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Container, Tabs } from "@mantine/core" import { Container, Tabs } from "@mantine/core"
import { TbCode, TbPhoto, TbUser } from "react-icons/tb" import { TbBell, TbCode, TbPhoto, TbUser } from "react-icons/tb"
import { CustomCodeSettings } from "@/components/settings/CustomCodeSettings" import { CustomCodeSettings } from "@/components/settings/CustomCodeSettings"
import { DisplaySettings } from "@/components/settings/DisplaySettings" import { DisplaySettings } from "@/components/settings/DisplaySettings"
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 (
@@ -13,6 +14,9 @@ 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="push-notifications" leftSection={<TbBell size={16} />}>
<Trans>Push notifications</Trans>
</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>
</Tabs.Tab> </Tabs.Tab>
@@ -25,6 +29,10 @@ export function SettingsPage() {
<DisplaySettings /> <DisplaySettings />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="push-notifications" pt="xl">
<PushNotificationSettings />
</Tabs.Panel>
<Tabs.Panel value="customCode" pt="xl"> <Tabs.Panel value="customCode" pt="xl">
<CustomCodeSettings /> <CustomCodeSettings />
</Tabs.Panel> </Tabs.Panel>

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.
*/ */
@@ -138,10 +144,9 @@ public interface CommaFeedConfiguration {
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal * Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
* resources. * resources.
* *
* You may want to disable this if you subscribe to feeds that are only available on your local network and you trust all users of * You may want to enable this if you host a public instance of CommaFeed with regisration open.
* your CommaFeed instance.
*/ */
@WithDefault("true") @WithDefault("false")
boolean blockLocalAddresses(); boolean blockLocalAddresses();
/** /**
@@ -242,6 +247,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 -> {
log.error("error while processing feed {}", feed.getUrl(), ex); 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);
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

@@ -1,5 +1,6 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -27,8 +28,6 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
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.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;
@@ -44,7 +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 WebSocketSessions webSocketSessions;
private final Striped<Lock> locks; private final Striped<Lock> locks;
@@ -52,12 +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, WebSocketSessions webSocketSessions) { FeedSubscriptionDAO feedSubscriptionDAO) {
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.webSocketSessions = webSocketSessions;
locks = Striped.lazyWeakLock(100000); locks = Striped.lazyWeakLock(100000);
@@ -67,7 +64,7 @@ public class FeedRefreshUpdater {
private AddEntryResult addEntry(final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) { private AddEntryResult addEntry(final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) {
boolean processed = false; boolean processed = false;
boolean inserted = false; FeedEntry insertedEntry = null;
Set<FeedSubscription> subscriptionsForWhichEntryIsUnread = new HashSet<>(); Set<FeedSubscription> subscriptionsForWhichEntryIsUnread = new HashSet<>();
// lock on feed, make sure we are not updating the same feed twice at // lock on feed, make sure we are not updating the same feed twice at
@@ -90,23 +87,21 @@ public class FeedRefreshUpdater {
locked2 = lock2.tryLock(1, TimeUnit.MINUTES); locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) { if (locked1 && locked2) {
processed = true; processed = true;
inserted = unitOfWork.call(() -> { insertedEntry = unitOfWork.call(() -> {
boolean newEntry = false; if (feedEntryService.find(feed, entry) != null) {
FeedEntry feedEntry = feedEntryService.find(feed, entry); // entry already exists, nothing to do
if (feedEntry == null) { return null;
feedEntry = feedEntryService.create(feed, entry);
newEntry = true;
} }
if (newEntry) {
entryInserted.mark(); FeedEntry feedEntry = feedEntryService.create(feed, entry);
for (FeedSubscription sub : subscriptions) { entryInserted.mark();
boolean unread = feedEntryService.applyFilter(sub, feedEntry); for (FeedSubscription sub : subscriptions) {
if (unread) { boolean unread = feedEntryService.applyFilter(sub, feedEntry);
subscriptionsForWhichEntryIsUnread.add(sub); if (unread) {
} subscriptionsForWhichEntryIsUnread.add(sub);
} }
} }
return newEntry; return feedEntry;
}); });
} else { } else {
log.error("lock timeout for {} - {}", feed.getUrl(), key1); log.error("lock timeout for {} - {}", feed.getUrl(), key1);
@@ -122,13 +117,13 @@ public class FeedRefreshUpdater {
lock2.unlock(); lock2.unlock();
} }
} }
return new AddEntryResult(processed, inserted, 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<>();
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
List<FeedSubscription> subscriptions = null; List<FeedSubscription> subscriptions = null;
@@ -138,8 +133,12 @@ public class FeedRefreshUpdater {
} }
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions); AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
processed &= addEntryResult.processed; processed &= addEntryResult.processed;
inserted += addEntryResult.inserted ? 1 : 0; inserted += addEntryResult.insertedEntry != null ? 1 : 0;
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum)); addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> {
if (addEntryResult.insertedEntry != null) {
insertedUnreadEntriesBySubscription.computeIfAbsent(sub, k -> new ArrayList<>()).add(addEntryResult.insertedEntry);
}
});
} }
if (inserted == 0) { if (inserted == 0) {
@@ -160,17 +159,13 @@ public class FeedRefreshUpdater {
unitOfWork.run(() -> feedService.update(feed)); unitOfWork.run(() -> feedService.update(feed));
notifyOverWebsocket(unreadCountBySubscription); return new FeedRefreshUpdaterResult(insertedUnreadEntriesBySubscription);
return processed;
} }
private void notifyOverWebsocket(Map<FeedSubscription, Long> unreadCountBySubscription) { private record AddEntryResult(boolean processed, FeedEntry insertedEntry, Set<FeedSubscription> subscriptionsForWhichEntryIsUnread) {
unreadCountBySubscription.forEach((sub, unreadCount) -> webSocketSessions.sendMessage(sub.getUser(),
WebSocketMessageBuilder.newFeedEntries(sub, unreadCount)));
} }
private record AddEntryResult(boolean processed, boolean inserted, 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,4 +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 = "push_notifications_enabled")
private boolean pushNotificationsEnabled;
} }

View File

@@ -77,6 +77,17 @@ public class UserSettings extends AbstractModel {
ON_MOBILE ON_MOBILE
} }
public enum PushNotificationType {
@JsonProperty("ntfy")
NTFY,
@JsonProperty("gotify")
GOTIFY,
@JsonProperty("pushover")
PUSHOVER
}
@OneToOne(fetch = FetchType.LAZY) @OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false, unique = true) @JoinColumn(name = "user_id", nullable = false, unique = true)
private User user; private User user;
@@ -133,6 +144,22 @@ public class UserSettings extends AbstractModel {
private boolean unreadCountFavicon; private boolean unreadCountFavicon;
private boolean disablePullToRefresh; private boolean disablePullToRefresh;
@Enumerated(EnumType.STRING)
@Column(name = "push_notification_type", length = 16)
private PushNotificationType pushNotificationType;
@Column(name = "push_notification_server_url", length = 1024)
private String pushNotificationServerUrl;
@Column(name = "push_notification_user_id", length = 512)
private String pushNotificationUserId;
@Column(name = "push_notification_user_secret", length = 512)
private String pushNotificationUserSecret;
@Column(name = "push_notification_topic", length = 256)
private String pushNotificationTopic;
private boolean email; private boolean email;
private boolean gmail; private boolean gmail;
private boolean facebook; private boolean facebook;

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); 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,15 +49,7 @@ public class FeedSubscriptionService {
}); });
} }
public long subscribe(User user, String url, String title) { public long subscribe(User user, String url, String title, FeedCategory category, int position, boolean pushNotificationsEnabled) {
return subscribe(user, url, title, null, 0);
}
public long subscribe(User user, String url, String title, FeedCategory parent) {
return subscribe(user, url, title, parent, 0);
}
public long subscribe(User user, String url, String title, FeedCategory category, int position) {
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)",
@@ -81,6 +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.setPushNotificationsEnabled(pushNotificationsEnabled);
return feedSubscriptionDAO.merge(sub).getId(); return feedSubscriptionDAO.merge(sub).getId();
} }

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,6 +82,28 @@ 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 = "push notification settings", required = true)
private PushNotificationSettings pushNotificationSettings = new PushNotificationSettings();
@Schema(description = "User notification settings")
@Data
public static class PushNotificationSettings implements Serializable {
@Schema(description = "notification provider type")
private PushNotificationType type;
@Schema(description = "server URL for ntfy or gotify")
private String serverUrl;
@Schema(description = "user Id")
private String userId;
@Schema(description = "user secret for authentication with the service")
private String userSecret;
@Schema(description = "topic")
private String topic;
}
@Schema(description = "User sharing settings") @Schema(description = "User sharing settings")
@Data @Data
public static class SharingSettings implements Serializable { public static class SharingSettings implements Serializable {

View File

@@ -65,6 +65,9 @@ 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 push notifications for new entries of this feed", required = true)
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();
Feed feed = subscription.getFeed(); Feed feed = subscription.getFeed();
@@ -85,6 +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.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
return sub; return sub;
} }

View File

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

View File

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

View File

@@ -365,7 +365,8 @@ 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); long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0,
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);
@@ -384,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()); 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());
} }
@@ -438,6 +439,8 @@ public class FeedREST {
subscription.setFilterLegacy(null); subscription.setFilterLegacy(null);
} }
subscription.setPushNotificationsEnabled(req.isPushNotificationsEnabled());
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

@@ -125,6 +125,12 @@ public class UserREST {
s.setUnreadCountFavicon(settings.isUnreadCountFavicon()); s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor()); s.setPrimaryColor(settings.getPrimaryColor());
s.getPushNotificationSettings().setType(settings.getPushNotificationType());
s.getPushNotificationSettings().setServerUrl(settings.getPushNotificationServerUrl());
s.getPushNotificationSettings().setUserId(settings.getPushNotificationUserId());
s.getPushNotificationSettings().setUserSecret(settings.getPushNotificationUserSecret());
s.getPushNotificationSettings().setTopic(settings.getPushNotificationTopic());
} else { } else {
s.setReadingMode(ReadingMode.UNREAD); s.setReadingMode(ReadingMode.UNREAD);
s.setReadingOrder(ReadingOrder.DESC); s.setReadingOrder(ReadingOrder.DESC);
@@ -190,6 +196,12 @@ public class UserREST {
s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor()); s.setPrimaryColor(settings.getPrimaryColor());
s.setPushNotificationType(settings.getPushNotificationSettings().getType());
s.setPushNotificationServerUrl(settings.getPushNotificationSettings().getServerUrl());
s.setPushNotificationUserId(settings.getPushNotificationSettings().getUserId());
s.setPushNotificationUserSecret(settings.getPushNotificationSettings().getUserSecret());
s.setPushNotificationTopic(settings.getPushNotificationSettings().getTopic());
s.setEmail(settings.getSharingSettings().isEmail()); s.setEmail(settings.getSharingSettings().isEmail());
s.setGmail(settings.getSharingSettings().isGmail()); s.setGmail(settings.getSharingSettings().isGmail());
s.setFacebook(settings.getSharingSettings().isFacebook()); s.setFacebook(settings.getSharingSettings().isFacebook());

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

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

@@ -46,7 +46,8 @@ class OPMLImporterTest {
importer.importOpml(user, xml); importer.importOpml(user, xml);
Mockito.verify(feedSubscriptionService) Mockito.verify(feedSubscriptionService)
.subscribe(Mockito.eq(user), Mockito.anyString(), Mockito.anyString(), Mockito.any(FeedCategory.class), Mockito.anyInt()); .subscribe(Mockito.eq(user), Mockito.anyString(), Mockito.anyString(), Mockito.any(FeedCategory.class), Mockito.anyInt(),
Mockito.anyBoolean());
} }
} }

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