"use client"; import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Bell, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { PasswordInput } from "@/components/ui/password-input"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { TestConnectionButton } from "@/components/settings/test-connection-button"; import { optimisticallySetChannelEnabled, persistChannelEnabled, rollbackChannelEnabled, } from "@/components/settings/notification-channel-toggle"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch"; interface NtfySettings { enabled: boolean; serverUrl: string; topic: string; hasAuthToken: boolean; } export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) { const { t } = useTranslations(); const queryClient = useQueryClient(); const [serverUrl, setServerUrl] = useState("https://ntfy.sh"); const [topic, setTopic] = useState(""); const [authToken, setAuthToken] = useState(""); const [saveMsg, setSaveMsg] = useState(null); const [saveMsgType, setSaveMsgType] = useState<"success" | "error" | null>( null, ); const { data: settings } = useQuery({ queryKey: queryKeys.settingsNtfy(), queryFn: async () => { return apiGet("/api/settings/ntfy"); }, enabled: isAuthenticated, }); // React-recommended sync-from-server pattern (no setState-in-effect). const settingsKey = settings ? `${settings.serverUrl}|${settings.topic}` : null; const [seededKey, setSeededKey] = useState(null); if (settingsKey && settingsKey !== seededKey) { setSeededKey(settingsKey); setServerUrl(settings!.serverUrl); setTopic(settings!.topic); } const save = useMutation({ mutationFn: async (enabled: boolean) => { const res = await apiFetchRaw("/api/settings/ntfy", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ serverUrl, topic, authToken: authToken || undefined, enabled, }), }); if (!res.ok) { const json = (await res.json()) as { error?: string; meta?: { errorCode?: string }; }; // A coded refusal (the private-origin policy) translates like the // test button's; anything else shows the server sentence. const code = json.meta?.errorCode; const key = code ? `settings.testConnection.errors.${code}` : null; const translated = key ? t(key) : null; throw new Error( translated && translated !== key ? translated : json.error || t("common.error"), ); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.settingsNtfy() }); setSaveMsg(t("settings.saved")); setSaveMsgType("success"); setAuthToken(""); }, onError: (err: Error) => { setSaveMsg(err.message); setSaveMsgType("error"); }, }); const toggleEnabled = useMutation< void, Error, boolean, NtfySettings | undefined >({ mutationFn: (enabled) => persistChannelEnabled("/api/settings/ntfy", enabled, t("common.error")), onMutate: async (enabled) => { const queryKey = queryKeys.settingsNtfy(); await queryClient.cancelQueries({ queryKey }); setSaveMsg(null); setSaveMsgType(null); return optimisticallySetChannelEnabled( queryClient, queryKey, enabled, ); }, onSuccess: () => { setSaveMsg(t("settings.saved")); setSaveMsgType("success"); }, onError: (err, _enabled, previous) => { rollbackChannelEnabled(queryClient, queryKeys.settingsNtfy(), previous); setSaveMsg(err.message); setSaveMsgType("error"); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: queryKeys.settingsNtfy() }); }, }); return (
toggleEnabled.mutate(checked)} disabled={save.isPending || toggleEnabled.isPending} />
{ e.preventDefault(); save.mutate(settings?.enabled ?? false); }} className="space-y-4" >
setServerUrl(e.target.value)} />
setTopic(e.target.value)} />
setAuthToken(e.target.value)} />

{t("settings.ntfyAuthTokenHint")}

{saveMsg && (

{saveMsg}

)}
); }