import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Input, InputNumber, Select, Space, Switch, Tabs } from 'antd'; import { BellOutlined, SettingOutlined } from '@ant-design/icons'; import { LanguageManager } from '@/utils'; import type { AllSetting } from '@/models/setting'; import { SettingListItem } from '@/components/ui'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { catTabLabel } from './catTabLabel'; interface TelegramTabProps { allSetting: AllSetting; updateSetting: (patch: Partial) => void; } // The notification schedule is fed straight to robfig/cron's AddJob (see // web.go startTask), which accepts @every , the @hourly/@daily/... // macros, and full crontab expressions. This builder covers the common cases // with dropdowns so users don't have to memorise the syntax, while "Custom" // preserves the raw crontab escape hatch. type Unit = 's' | 'm' | 'h'; type Macro = '@hourly' | '@daily' | '@weekly' | '@monthly'; type Mode = 'every' | Macro | 'custom'; const MACROS: Macro[] = ['@hourly', '@daily', '@weekly', '@monthly']; const EVERY_RE = /^@every\s+(\d+)\s*([smh])$/i; interface RunTime { mode: Mode; num: number; unit: Unit; custom: string; } function parseRunTime(raw: string): RunTime { const v = (raw ?? '').trim(); const m = v.match(EVERY_RE); if (m) { return { mode: 'every', num: Math.max(1, Number(m[1]) || 1), unit: m[2].toLowerCase() as Unit, custom: '' }; } if ((MACROS as string[]).includes(v)) { return { mode: v as Macro, num: 1, unit: 'h', custom: '' }; } return { mode: 'custom', num: 1, unit: 'h', custom: v }; } function composeRunTime(s: RunTime): string { if (s.mode === 'every') return `@every ${Math.max(1, s.num || 1)}${s.unit}`; if (s.mode === 'custom') return s.custom; return s.mode; } // The panel's cron runs with seconds enabled (cron.WithSeconds() in web.go), so // crontab expressions are 6-field: "second minute hour day month weekday". When // the user drops into Custom we seed the box with the crontab equivalent of the // current selection rather than a bare @macro, so they get a real expression to // edit (and one that the 6-field parser accepts). function toCrontab(s: RunTime): string { switch (s.mode) { case '@hourly': return '0 0 * * * *'; case '@daily': return '0 0 0 * * *'; case '@weekly': return '0 0 0 * * 0'; case '@monthly': return '0 0 0 1 * *'; case 'every': { const n = Math.max(1, s.num || 1); if (s.unit === 's') return `*/${n} * * * * *`; if (s.unit === 'm') return `0 */${n} * * * *`; return `0 0 */${n} * * *`; } default: return s.custom; } } function NotifyTimeField({ value, onChange }: { value: string; onChange: (v: string) => void }) { const { t } = useTranslation(); // Init once: the Settings tabs only mount after settings are fetched, so the // incoming value is already the persisted one. const [state, setState] = useState(() => parseRunTime(value)); function update(patch: Partial) { const next = { ...state, ...patch }; setState(next); onChange(composeRunTime(next)); } function onModeChange(mode: Mode) { // Seed Custom with the crontab equivalent of the current selection so the // box starts from a real expression (e.g. "0 0 0 * * *", not "@daily"). if (mode === 'custom' && !state.custom.trim()) { update({ mode, custom: toCrontab(state) }); } else { update({ mode }); } } const modeOptions = [ { value: 'every', label: t('pages.settings.notifyTime.every') }, { value: '@hourly', label: t('pages.settings.notifyTime.hourly') }, { value: '@daily', label: t('pages.settings.notifyTime.daily') }, { value: '@weekly', label: t('pages.settings.notifyTime.weekly') }, { value: '@monthly', label: t('pages.settings.notifyTime.monthly') }, { value: 'custom', label: t('pages.settings.notifyTime.custom') }, ]; const unitOptions = [ { value: 's', label: t('pages.settings.notifyTime.seconds') }, { value: 'm', label: t('pages.settings.notifyTime.minutes') }, { value: 'h', label: t('pages.settings.notifyTime.hours') }, ]; return ( style={{ width: '100%' }} value={state.mode} options={modeOptions} onChange={onModeChange} /> {state.mode === 'every' && ( update({ num: Math.max(1, Number(v) || 1) })} /> style={{ width: '50%' }} value={state.unit} options={unitOptions} onChange={(unit) => update({ unit })} /> )} {state.mode === 'custom' && ( update({ custom: e.target.value })} /> )} ); } export default function TelegramTab({ allSetting, updateSetting }: TelegramTabProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const langOptions = useMemo( () => LanguageManager.supportedLanguages.map((l: { value: string; name: string; icon: string }) => ({ value: l.value, label: ( <> {l.icon}   {l.name} ), })), [], ); return ( , t('pages.settings.panelSettings'), isMobile), children: ( <> updateSetting({ tgBotEnable: v })} /> updateSetting({ tgBotToken: e.target.value })} /> updateSetting({ tgBotChatId: e.target.value })} /> updateSetting({ tgBotAPIServer: e.target.value })} /> ), }, { key: '2', label: catTabLabel(, t('pages.settings.notifications'), isMobile), children: ( <> updateSetting({ tgRunTime: v })} /> updateSetting({ tgBotBackup: v })} /> updateSetting({ tgBotLoginNotify: v })} /> updateSetting({ tgCpu: Number(v) || 0 })} /> ), }, ]} /> ); }