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

View File

@@ -0,0 +1,19 @@
import { Trans } from "@lingui/react/macro"
import { Checkbox, type CheckboxProps } from "@mantine/core"
import type { ReactNode } from "react"
import { useAppSelector } from "@/app/store"
export const ReceivePushNotificationsChechbox = (props: CheckboxProps) => {
const pushNotificationsEnabled = useAppSelector(state => state.server.serverInfos?.pushNotificationsEnabled)
const pushNotificationsConfigured = useAppSelector(state => !!state.user.settings?.pushNotificationSettings.type)
const disabled = !pushNotificationsEnabled || !pushNotificationsConfigured
let description: ReactNode = ""
if (!pushNotificationsEnabled) {
description = <Trans>Push notifications are not enabled on this CommaFeed instance.</Trans>
} else if (!pushNotificationsConfigured) {
description = <Trans>Push notifications are not configured in your user settings.</Trans>
}
return <Checkbox label={<Trans>Receive push notifications</Trans>} disabled={disabled} description={description} {...props} />
}

View File

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

View File

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

View File

@@ -0,0 +1,93 @@
import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro"
import { Button, Group, Select, Stack, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form"
import { useEffect } from "react"
import { TbDeviceFloppy } from "react-icons/tb"
import { redirectToSelectedSource } from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store"
import type { PushNotificationSettings as PushNotificationSettingsModel } from "@/app/types"
import { changeNotificationSettings } from "@/app/user/thunks"
export function PushNotificationSettings() {
const notificationSettings = useAppSelector(state => state.user.settings?.pushNotificationSettings)
const pushNotificationsEnabled = useAppSelector(state => state.server.serverInfos?.pushNotificationsEnabled)
const { _ } = useLingui()
const dispatch = useAppDispatch()
const form = useForm<PushNotificationSettingsModel>()
useEffect(() => {
if (notificationSettings) form.initialize(notificationSettings)
}, [form.initialize, notificationSettings])
const handleSubmit = (values: PushNotificationSettingsModel) => {
dispatch(changeNotificationSettings(values))
}
const typeInputProps = form.getInputProps("type")
if (!pushNotificationsEnabled) {
return <Trans>Push notifications are not enabled on this CommaFeed instance.</Trans>
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<Select
label={<Trans>Push notification service</Trans>}
data={[
{ value: "ntfy", label: "ntfy" },
{ value: "gotify", label: "Gotify" },
{ value: "pushover", label: "Pushover" },
]}
clearable
{...typeInputProps}
onChange={value => {
typeInputProps.onChange(value)
form.setFieldValue("serverUrl", "")
form.setFieldValue("topic", "")
form.setFieldValue("userSecret", "")
form.setFieldValue("userId", "")
}}
/>
{form.values.type === "ntfy" && (
<>
<TextInput
label={<Trans>Server URL</Trans>}
placeholder="https://ntfy.sh"
required
{...form.getInputProps("serverUrl")}
/>
<TextInput label={<Trans>Topic</Trans>} placeholder="commafeed" required {...form.getInputProps("topic")} />
<TextInput label={<Trans>Access token</Trans>} {...form.getInputProps("userSecret")} />
</>
)}
{form.values.type === "gotify" && (
<>
<TextInput
label={<Trans>Server URL</Trans>}
placeholder="https://gotify.example.com"
required
{...form.getInputProps("serverUrl")}
/>
<TextInput label={<Trans>App token</Trans>} required {...form.getInputProps("userSecret")} />
</>
)}
{form.values.type === "pushover" && (
<>
<TextInput label={<Trans>User key</Trans>} required {...form.getInputProps("userId")} />
<TextInput label={<Trans>API token</Trans>} required {...form.getInputProps("userSecret")} />
</>
)}
<Group>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftSection={<TbDeviceFloppy size={16} />}>
<Trans>Save</Trans>
</Button>
</Group>
</Stack>
</form>
)
}