From 8f3c4d83a7d13f550d920624c79612c220831d45 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 16 Aug 2025 15:52:27 +0200 Subject: [PATCH 1/7] feat: add push notification settings panel integration - Add PushNotificationSettings interface to settings types - Update useGenericSettings hook to support push_notifications group - Create comprehensive PushNotificationSettings component with: - General enable/disable toggle - Service configuration (worker count, queue size, retry settings) - APNs configuration for iOS push notifications - FCM configuration for Android push notifications - Integrate push notifications panel into AdvancedSettingsLayout - Follow existing settings patterns for consistency - Include proper validation and form handling --- .../settings/PushNotificationSettings.tsx | 414 ++++++++++++++++++ .../layouts/AdvancedSettingsLayout.tsx | 5 + src/hooks/useGenericSettings.ts | 11 + src/types/settings.types.ts | 28 +- 4 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/PushNotificationSettings.tsx diff --git a/src/components/settings/PushNotificationSettings.tsx b/src/components/settings/PushNotificationSettings.tsx new file mode 100644 index 00000000..b30f3912 --- /dev/null +++ b/src/components/settings/PushNotificationSettings.tsx @@ -0,0 +1,414 @@ +import React, { useEffect, useState } from 'react'; +import { Form, Input, InputNumber, Switch, Tooltip, Card, Divider } from 'antd'; +import { + QuestionCircleOutlined, + BellOutlined, + AppleOutlined, + AndroidOutlined, + SettingOutlined, + KeyOutlined, + FileTextOutlined +} from '@ant-design/icons'; +import useGenericSettings from '@app/hooks/useGenericSettings'; +import { SettingsGroupType } from '@app/types/settings.types'; +import BaseSettingsForm from './BaseSettingsForm'; +import * as S from './Settings.styles'; + +const PushNotificationSettings: React.FC = () => { + const { + settings, + loading, + error, + fetchSettings, + updateSettings, + saveSettings, + } = useGenericSettings('push_notifications'); + + const [form] = Form.useForm(); + const [isUserEditing, setIsUserEditing] = useState(false); + + // Update form values when settings change, but only if user isn't actively editing + useEffect(() => { + if (settings && !isUserEditing) { + console.log('PushNotificationSettings - Received settings:', settings); + + // The useGenericSettings hook returns the settings data + const settingsObj = settings as Record; + + console.log('PushNotificationSettings - Setting form values directly:', settingsObj); + + // Set form values directly + form.setFieldsValue(settingsObj); + console.log('PushNotificationSettings - Form values after set:', form.getFieldsValue()); + } + }, [settings, form, isUserEditing]); + + // Handle form value changes + const handleValuesChange = (changedValues: Partial>) => { + setIsUserEditing(true); // Mark that user is currently editing + updateSettings(changedValues); + }; + + // Modified save function to reset the editing flag + const handleSave = async () => { + await saveSettings(); + setIsUserEditing(false); // Reset after saving + }; + + return ( + { + fetchSettings(); + setIsUserEditing(false); + }} + > +
{ + console.log('Form submitted with values:', values); + setIsUserEditing(false); + }} + > + {/* Main Configuration */} + + + General Configuration + + } + style={{ marginBottom: 16 }} + > + + Enable Push Notifications  + + + + + } + valuePropName="checked" + > + + + + + {/* Service Configuration */} + + + Service Configuration + + } + style={{ marginBottom: 16 }} + > + + Worker Count  + + + + + } + rules={[ + { required: true, message: 'Please enter worker count' }, + { type: 'number', min: 1, max: 100, message: 'Worker count must be between 1 and 100' } + ]} + > + + + + + Queue Size  + + + + + } + rules={[ + { required: true, message: 'Please enter queue size' }, + { type: 'number', min: 100, max: 10000, message: 'Queue size must be between 100 and 10000' } + ]} + > + + + + + Max Retry Attempts  + + + + + } + rules={[ + { required: true, message: 'Please enter max retry attempts' }, + { type: 'number', min: 1, max: 10, message: 'Max retry attempts must be between 1 and 10' } + ]} + > + + + + + Retry Base Delay  + + + + + } + rules={[ + { required: true, message: 'Please enter retry base delay' }, + { + pattern: /^\d+[a-zA-Z]+$/, + message: 'Invalid duration format. Use Go duration format (e.g., "1s", "500ms", "2m")' + } + ]} + > + } + /> + + + + {/* APNs Configuration */} + + + Apple Push Notification Service (APNs) + + } + style={{ marginBottom: 16 }} + > + + Enable APNs  + + + + + } + valuePropName="checked" + > + + + + + APNs Key File Path  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter APNs key file path when APNs is enabled' + } + ]} + > + } + /> + + + + APNs Key ID  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter APNs key ID when APNs is enabled' + }, + { len: 10, message: 'APNs Key ID must be exactly 10 characters' } + ]} + > + } + maxLength={10} + /> + + + + Team ID  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter Team ID when APNs is enabled' + }, + { len: 10, message: 'Team ID must be exactly 10 characters' } + ]} + > + } + maxLength={10} + /> + + + + App Bundle Identifier  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter app bundle identifier when APNs is enabled' + } + ]} + > + } + /> + + + + Production Mode  + + + + + } + valuePropName="checked" + > + + + + + {/* FCM Configuration */} + + + Firebase Cloud Messaging (FCM) + + } + style={{ marginBottom: 16 }} + > + + Enable FCM  + + + + + } + valuePropName="checked" + > + + + + + FCM Credentials File Path  + + + + + } + rules={[ + { + required: form.getFieldValue(['fcm', 'enabled']), + message: 'Please enter FCM credentials file path when FCM is enabled' + } + ]} + > + } + /> + + + + +

+ Note: Push notification settings are saved to the configuration file + and the push notification service automatically reloads with the new configuration. + At least one service (APNs or FCM) should be enabled if push notifications are enabled. +

+
+
+
+ ); +}; + +export default PushNotificationSettings; \ No newline at end of file diff --git a/src/components/settings/layouts/AdvancedSettingsLayout.tsx b/src/components/settings/layouts/AdvancedSettingsLayout.tsx index 128d8fe6..757e308b 100644 --- a/src/components/settings/layouts/AdvancedSettingsLayout.tsx +++ b/src/components/settings/layouts/AdvancedSettingsLayout.tsx @@ -8,6 +8,7 @@ import ImageModerationPanel from '../panels/ImageModerationPanel'; import ContentFilterPanel from '../panels/ContentFilterPanel'; import OllamaPanel from '../panels/OllamaPanel'; import WalletPanel from '../panels/WalletPanel'; +import PushNotificationSettings from '../PushNotificationSettings'; import useGenericSettings from '@app/hooks/useGenericSettings'; const { Panel } = Collapse; @@ -149,6 +150,10 @@ const AdvancedSettingsLayout: React.FC = ({ + + + + diff --git a/src/hooks/useGenericSettings.ts b/src/hooks/useGenericSettings.ts index 46b86f74..980c7332 100644 --- a/src/hooks/useGenericSettings.ts +++ b/src/hooks/useGenericSettings.ts @@ -36,6 +36,10 @@ const extractSettingsForGroup = (settings: any, groupName: string) => { rawData = settings?.server || {}; break; + case 'push_notifications': + rawData = settings?.push_notifications || {}; + break; + default: console.warn(`Unknown settings group: ${groupName}`); return {}; @@ -331,6 +335,13 @@ const buildNestedUpdate = (groupName: string, data: any) => { return result; + case 'push_notifications': + return { + settings: { + push_notifications: data + } + }; + default: console.warn(`Unknown settings group for save: ${groupName}`); return { diff --git a/src/types/settings.types.ts b/src/types/settings.types.ts index be49f40c..503b1410 100644 --- a/src/types/settings.types.ts +++ b/src/types/settings.types.ts @@ -87,25 +87,49 @@ export interface GeneralSettings { relay_stats_db: string; } +export interface PushNotificationSettings { + enabled: boolean; + service: { + worker_count: number; + queue_size: number; + retry_max_attempts: number; + retry_base_delay: string; + }; + apns: { + enabled: boolean; + key_file: string; + key_id: string; + team_id: string; + topic: string; + production: boolean; + }; + fcm: { + enabled: boolean; + credentials_file: string; + }; +} + export interface QueryCacheSettings { [key: string]: any; } -export type SettingsGroupName = +export type SettingsGroupName = | 'image_moderation' | 'content_filter' | 'ollama' | 'relay_info' | 'wallet' | 'general' + | 'push_notifications' | 'relay_settings'; -export type SettingsGroupType = +export type SettingsGroupType = T extends 'image_moderation' ? ImageModerationSettings : T extends 'content_filter' ? ContentFilterSettings : T extends 'ollama' ? OllamaSettings : T extends 'relay_info' ? RelayInfoSettings : T extends 'wallet' ? WalletSettings : T extends 'general' ? GeneralSettings : + T extends 'push_notifications' ? PushNotificationSettings : T extends 'relay_settings' ? any : // Using any for relay_settings as it's already defined elsewhere never; From ccbd74a2888a3ffdded8e5c6f9051c5d93e060eb Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 16 Aug 2025 16:49:42 +0200 Subject: [PATCH 2/7] feat: fix push notification panel integration structure - Create proper PushNotificationPanel component in panels directory - Update AdvancedSettingsLayout to use PushNotificationPanel instead of PushNotificationSettings - Fix import path to use correct BaseSettingsPanel location - Follow established panel architecture patterns for consistent UI integration --- .../layouts/AdvancedSettingsLayout.tsx | 4 +- .../settings/panels/PushNotificationPanel.tsx | 419 ++++++++++++++++++ 2 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/panels/PushNotificationPanel.tsx diff --git a/src/components/settings/layouts/AdvancedSettingsLayout.tsx b/src/components/settings/layouts/AdvancedSettingsLayout.tsx index 757e308b..8ca49766 100644 --- a/src/components/settings/layouts/AdvancedSettingsLayout.tsx +++ b/src/components/settings/layouts/AdvancedSettingsLayout.tsx @@ -8,7 +8,7 @@ import ImageModerationPanel from '../panels/ImageModerationPanel'; import ContentFilterPanel from '../panels/ContentFilterPanel'; import OllamaPanel from '../panels/OllamaPanel'; import WalletPanel from '../panels/WalletPanel'; -import PushNotificationSettings from '../PushNotificationSettings'; +import PushNotificationPanel from '../panels/PushNotificationPanel'; import useGenericSettings from '@app/hooks/useGenericSettings'; const { Panel } = Collapse; @@ -152,7 +152,7 @@ const AdvancedSettingsLayout: React.FC = ({ - + diff --git a/src/components/settings/panels/PushNotificationPanel.tsx b/src/components/settings/panels/PushNotificationPanel.tsx new file mode 100644 index 00000000..0a993386 --- /dev/null +++ b/src/components/settings/panels/PushNotificationPanel.tsx @@ -0,0 +1,419 @@ +import React, { useEffect, useState } from 'react'; +import { Form, Input, InputNumber, Switch, Tooltip, Card } from 'antd'; +import { + QuestionCircleOutlined, + BellOutlined, + AppleOutlined, + AndroidOutlined, + SettingOutlined, + KeyOutlined, + FileTextOutlined +} from '@ant-design/icons'; +import useGenericSettings from '@app/hooks/useGenericSettings'; +import { SettingsGroupType } from '@app/types/settings.types'; +import BaseSettingsPanel from '../BaseSettingsPanel'; + +const PushNotificationPanel: React.FC = () => { + const { + settings, + loading, + error, + updateSettings, + saveSettings: savePushNotificationSettings, + } = useGenericSettings('push_notifications'); + + const [form] = Form.useForm(); + const [isUserEditing, setIsUserEditing] = useState(false); + + // Listen for global save event + useEffect(() => { + const handleGlobalSave = () => { + setTimeout(() => { + setIsUserEditing(false); + }, 200); + }; + + document.addEventListener('settings-saved', handleGlobalSave); + + return () => { + document.removeEventListener('settings-saved', handleGlobalSave); + }; + }, []); + + // Update form values when settings change, but only if user isn't actively editing + useEffect(() => { + if (settings && !isUserEditing) { + console.log('PushNotificationPanel - Received settings:', settings); + + // The useGenericSettings hook returns the settings data + const settingsObj = settings as Record; + + console.log('PushNotificationPanel - Setting form values directly:', settingsObj); + + // Set form values directly + form.setFieldsValue(settingsObj); + console.log('PushNotificationPanel - Form values after set:', form.getFieldsValue()); + } + }, [settings, form, isUserEditing]); + + // Handle form value changes + const handleValuesChange = (changedValues: Partial>) => { + setIsUserEditing(true); // Mark that user is currently editing + console.log('PushNotificationPanel - changedValues:', changedValues); + console.log('PushNotificationPanel - current form values:', form.getFieldsValue()); + updateSettings(changedValues); + }; + + return ( + +
{ + console.log('Form submitted with values:', values); + setIsUserEditing(false); + }} + > + {/* Main Configuration */} + + + General Configuration + + } + style={{ marginBottom: 16 }} + size="small" + > + + Enable Push Notifications  + + + + + } + valuePropName="checked" + > + + + + + {/* Service Configuration */} + + + Service Configuration + + } + style={{ marginBottom: 16 }} + size="small" + > + + Worker Count  + + + + + } + rules={[ + { required: true, message: 'Please enter worker count' }, + { type: 'number', min: 1, max: 100, message: 'Worker count must be between 1 and 100' } + ]} + > + + + + + Queue Size  + + + + + } + rules={[ + { required: true, message: 'Please enter queue size' }, + { type: 'number', min: 100, max: 10000, message: 'Queue size must be between 100 and 10000' } + ]} + > + + + + + Max Retry Attempts  + + + + + } + rules={[ + { required: true, message: 'Please enter max retry attempts' }, + { type: 'number', min: 1, max: 10, message: 'Max retry attempts must be between 1 and 10' } + ]} + > + + + + + Retry Base Delay  + + + + + } + rules={[ + { required: true, message: 'Please enter retry base delay' }, + { + pattern: /^\d+[a-zA-Z]+$/, + message: 'Invalid duration format. Use Go duration format (e.g., "1s", "500ms", "2m")' + } + ]} + > + } + /> + + + + {/* APNs Configuration */} + + + Apple Push Notification Service (APNs) + + } + style={{ marginBottom: 16 }} + size="small" + > + + Enable APNs  + + + + + } + valuePropName="checked" + > + + + + + APNs Key File Path  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter APNs key file path when APNs is enabled' + } + ]} + > + } + /> + + + + APNs Key ID  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter APNs key ID when APNs is enabled' + }, + { len: 10, message: 'APNs Key ID must be exactly 10 characters' } + ]} + > + } + maxLength={10} + /> + + + + Team ID  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter Team ID when APNs is enabled' + }, + { len: 10, message: 'Team ID must be exactly 10 characters' } + ]} + > + } + maxLength={10} + /> + + + + App Bundle Identifier  + + + + + } + rules={[ + { + required: form.getFieldValue(['apns', 'enabled']), + message: 'Please enter app bundle identifier when APNs is enabled' + } + ]} + > + } + /> + + + + Production Mode  + + + + + } + valuePropName="checked" + > + + + + + {/* FCM Configuration */} + + + Firebase Cloud Messaging (FCM) + + } + style={{ marginBottom: 16 }} + size="small" + > + + Enable FCM  + + + + + } + valuePropName="checked" + > + + + + + FCM Credentials File Path  + + + + + } + rules={[ + { + required: form.getFieldValue(['fcm', 'enabled']), + message: 'Please enter FCM credentials file path when FCM is enabled' + } + ]} + > + } + /> + + + + +

+ Note: Push notification settings require appropriate credentials and configuration files to function properly. Ensure you have the necessary APNs or FCM credentials configured on your server. +

+
+
+
+ ); +}; + +export default PushNotificationPanel; \ No newline at end of file From 1ba49fa3c301742e4d49a5bece9a51ee5b2962f7 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 16 Aug 2025 19:47:08 +0200 Subject: [PATCH 3/7] debug: add console logs to trace push notification panel rendering issue - Add console.log at component mount to verify if PushNotificationPanel is rendering - Log hook results to check if useGenericSettings is working correctly - These logs will help identify why the panel isn't appearing in the DOM --- src/components/settings/panels/PushNotificationPanel.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/settings/panels/PushNotificationPanel.tsx b/src/components/settings/panels/PushNotificationPanel.tsx index 0a993386..fdd76252 100644 --- a/src/components/settings/panels/PushNotificationPanel.tsx +++ b/src/components/settings/panels/PushNotificationPanel.tsx @@ -14,6 +14,8 @@ import { SettingsGroupType } from '@app/types/settings.types'; import BaseSettingsPanel from '../BaseSettingsPanel'; const PushNotificationPanel: React.FC = () => { + console.log('PushNotificationPanel - Component rendering'); + const { settings, loading, @@ -24,6 +26,8 @@ const PushNotificationPanel: React.FC = () => { const [form] = Form.useForm(); const [isUserEditing, setIsUserEditing] = useState(false); + + console.log('PushNotificationPanel - Hook result:', { settings, loading, error }); // Listen for global save event useEffect(() => { From 5c8b3b277dbb800f474180ecdead41107432df0f Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 16 Aug 2025 19:49:37 +0200 Subject: [PATCH 4/7] fix: remove unused saveSettings variable from PushNotificationPanel - Remove savePushNotificationSettings to fix TypeScript warning - Panel uses global save button like other panels without individual save - This warning may have been preventing the component from rendering --- src/components/settings/panels/PushNotificationPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/panels/PushNotificationPanel.tsx b/src/components/settings/panels/PushNotificationPanel.tsx index fdd76252..567b5fc9 100644 --- a/src/components/settings/panels/PushNotificationPanel.tsx +++ b/src/components/settings/panels/PushNotificationPanel.tsx @@ -21,7 +21,7 @@ const PushNotificationPanel: React.FC = () => { loading, error, updateSettings, - saveSettings: savePushNotificationSettings, + // saveSettings is not used in panels - they use the global save } = useGenericSettings('push_notifications'); const [form] = Form.useForm(); From d1747348e9280c8244abed1550f4eebc9a576279 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 16 Aug 2025 21:44:29 +0200 Subject: [PATCH 5/7] feat: complete push notification panel integration and data mapping - Fix push notification panel visibility by adding to SettingsNavigation and SettingsPage routing - Add comprehensive data transformation logic in useGenericSettings for nested-to-flat field mapping - Fix field naming consistency between PushNotificationSettings and PushNotificationPanel components - Add missing service_batch_size field to complete backend field coverage - Implement bidirectional data transformation for proper save/load functionality - Update settings types to include push_notifications group - Complete integration of all push notification configuration fields: * Main: enabled * Service: worker_count, queue_size, batch_size, retry_attempts, retry_delay * APNs: enabled, key_path, key_id, team_id, bundle_id, production * FCM: enabled, credentials_path, project_id Resolves panel visibility issues and ensures proper data flow between frontend and backend. --- .../settings/PushNotificationSettings.tsx | 119 ++++++++------ .../settings/SettingsNavigation.tsx | 7 + src/components/settings/SettingsPage.tsx | 9 ++ .../layouts/AdvancedSettingsLayout.tsx | 2 +- .../settings/panels/PushNotificationPanel.tsx | 152 ++++++------------ src/hooks/useGenericSettings.ts | 133 ++++++++++++++- src/types/settings.types.ts | 10 +- 7 files changed, 271 insertions(+), 161 deletions(-) diff --git a/src/components/settings/PushNotificationSettings.tsx b/src/components/settings/PushNotificationSettings.tsx index b30f3912..d6912100 100644 --- a/src/components/settings/PushNotificationSettings.tsx +++ b/src/components/settings/PushNotificationSettings.tsx @@ -113,7 +113,7 @@ const PushNotificationSettings: React.FC = () => { style={{ marginBottom: 16 }} > Worker Count  @@ -127,8 +127,8 @@ const PushNotificationSettings: React.FC = () => { { type: 'number', min: 1, max: 100, message: 'Worker count must be between 1 and 100' } ]} > - { Queue Size  @@ -150,8 +150,8 @@ const PushNotificationSettings: React.FC = () => { { type: 'number', min: 100, max: 10000, message: 'Queue size must be between 100 and 10000' } ]} > - { Max Retry Attempts  @@ -173,8 +173,8 @@ const PushNotificationSettings: React.FC = () => { { type: 'number', min: 1, max: 10, message: 'Max retry attempts must be between 1 and 10' } ]} > - { Retry Base Delay  @@ -193,17 +193,40 @@ const PushNotificationSettings: React.FC = () => { } rules={[ { required: true, message: 'Please enter retry base delay' }, - { - pattern: /^\d+[a-zA-Z]+$/, - message: 'Invalid duration format. Use Go duration format (e.g., "1s", "500ms", "2m")' + { + pattern: /^\d+[a-zA-Z]+$/, + message: 'Invalid duration format. Use Go duration format (e.g., "1s", "500ms", "2m")' } ]} > - } /> + + + Batch Size  + + + + + } + rules={[ + { required: true, message: 'Please enter batch size' }, + { type: 'number', min: 1, max: 1000, message: 'Batch size must be between 1 and 1000' } + ]} + > + + {/* APNs Configuration */} @@ -217,7 +240,7 @@ const PushNotificationSettings: React.FC = () => { style={{ marginBottom: 16 }} > Enable APNs  @@ -232,7 +255,7 @@ const PushNotificationSettings: React.FC = () => { APNs Key File Path  @@ -242,20 +265,20 @@ const PushNotificationSettings: React.FC = () => { } rules={[ - { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter APNs key file path when APNs is enabled' + { + required: form.getFieldValue('apns_enabled'), + message: 'Please enter APNs key file path when APNs is enabled' } ]} > - } /> APNs Key ID  @@ -265,22 +288,22 @@ const PushNotificationSettings: React.FC = () => { } rules={[ - { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter APNs key ID when APNs is enabled' + { + required: form.getFieldValue('apns_enabled'), + message: 'Please enter APNs key ID when APNs is enabled' }, { len: 10, message: 'APNs Key ID must be exactly 10 characters' } ]} > - } maxLength={10} /> Team ID  @@ -290,22 +313,22 @@ const PushNotificationSettings: React.FC = () => { } rules={[ - { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter Team ID when APNs is enabled' + { + required: form.getFieldValue('apns_enabled'), + message: 'Please enter Team ID when APNs is enabled' }, { len: 10, message: 'Team ID must be exactly 10 characters' } ]} > - } maxLength={10} /> App Bundle Identifier  @@ -315,20 +338,20 @@ const PushNotificationSettings: React.FC = () => { } rules={[ - { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter app bundle identifier when APNs is enabled' + { + required: form.getFieldValue('apns_enabled'), + message: 'Please enter app bundle identifier when APNs is enabled' } ]} > - } /> Production Mode  @@ -354,7 +377,7 @@ const PushNotificationSettings: React.FC = () => { style={{ marginBottom: 16 }} > Enable FCM  @@ -369,7 +392,7 @@ const PushNotificationSettings: React.FC = () => { FCM Credentials File Path  @@ -379,14 +402,14 @@ const PushNotificationSettings: React.FC = () => { } rules={[ - { - required: form.getFieldValue(['fcm', 'enabled']), - message: 'Please enter FCM credentials file path when FCM is enabled' + { + required: form.getFieldValue('fcm_enabled'), + message: 'Please enter FCM credentials file path when FCM is enabled' } ]} > - } /> diff --git a/src/components/settings/SettingsNavigation.tsx b/src/components/settings/SettingsNavigation.tsx index 82ff0c73..c116b353 100644 --- a/src/components/settings/SettingsNavigation.tsx +++ b/src/components/settings/SettingsNavigation.tsx @@ -11,6 +11,7 @@ import { InfoCircleOutlined, WalletOutlined, GlobalOutlined, + BellOutlined, } from '@ant-design/icons'; const { Panel } = Collapse; @@ -86,6 +87,12 @@ const settingsTabs: SettingsTab[] = [ icon: , path: '/settings/ollama' }, + { + key: 'push_notifications', + label: 'Push Notifications', + icon: , + path: '/settings/push-notifications' + }, { key: 'relay_info', label: 'Relay Info', diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 9e8ebd04..b06db9c2 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -9,6 +9,7 @@ import { InfoCircleOutlined, WalletOutlined, GlobalOutlined, + BellOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'; @@ -18,6 +19,7 @@ import OllamaSettings from './OllamaSettings'; import WalletSettings from './WalletSettings'; import GeneralSettings from './GeneralSettings'; import RelayInfoSettings from './RelayInfoSettings'; +import PushNotificationSettings from './PushNotificationSettings'; const SettingsContainer = styled.div` width: 100%; @@ -160,6 +162,13 @@ const SettingsPage: React.FC = () => { icon: , component: }, + { + key: 'push_notifications', + path: '/settings/push-notifications', + label: 'Push Notifications', + icon: , + component: + }, { key: 'relay_info', path: '/settings/relay-info', diff --git a/src/components/settings/layouts/AdvancedSettingsLayout.tsx b/src/components/settings/layouts/AdvancedSettingsLayout.tsx index 8ca49766..09727650 100644 --- a/src/components/settings/layouts/AdvancedSettingsLayout.tsx +++ b/src/components/settings/layouts/AdvancedSettingsLayout.tsx @@ -35,7 +35,7 @@ const AdvancedSettingsLayout: React.FC = ({ }) => { const [saveLoading, setSaveLoading] = useState(false); const [resetLoading, setResetLoading] = useState(false); - const [activeKeys, setActiveKeys] = useState(['general', 'image-moderation']); + const [activeKeys, setActiveKeys] = useState(['general', 'image-moderation', 'push-notifications']); // Use the generic settings hook to handle saving all settings const { diff --git a/src/components/settings/panels/PushNotificationPanel.tsx b/src/components/settings/panels/PushNotificationPanel.tsx index 567b5fc9..52ef10e6 100644 --- a/src/components/settings/panels/PushNotificationPanel.tsx +++ b/src/components/settings/panels/PushNotificationPanel.tsx @@ -122,7 +122,7 @@ const PushNotificationPanel: React.FC = () => { size="small" > Worker Count  @@ -145,7 +145,7 @@ const PushNotificationPanel: React.FC = () => { Queue Size  @@ -168,7 +168,7 @@ const PushNotificationPanel: React.FC = () => { Max Retry Attempts  @@ -191,7 +191,7 @@ const PushNotificationPanel: React.FC = () => { Retry Base Delay  @@ -213,6 +213,29 @@ const PushNotificationPanel: React.FC = () => { prefix={} /> + + + Batch Size  + + + + + } + rules={[ + { required: true, message: 'Please enter batch size' }, + { type: 'number', min: 1, max: 1000, message: 'Batch size must be between 1 and 1000' } + ]} + > + + {/* APNs Configuration */} @@ -227,11 +250,11 @@ const PushNotificationPanel: React.FC = () => { size="small" > Enable APNs  - + @@ -242,115 +265,48 @@ const PushNotificationPanel: React.FC = () => { APNs Key File Path  - - - - - } - rules={[ - { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter APNs key file path when APNs is enabled' - } - ]} - > - } - /> - - - - APNs Key ID  - + } rules={[ - { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter APNs key ID when APNs is enabled' - }, - { len: 10, message: 'APNs Key ID must be exactly 10 characters' } - ]} - > - } - maxLength={10} - /> - - - - Team ID  - - - - - } - rules={[ - { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter Team ID when APNs is enabled' - }, - { len: 10, message: 'Team ID must be exactly 10 characters' } + { required: false, message: 'Please enter the APNs key file path' } ]} > } - maxLength={10} /> - App Bundle Identifier  - + Bundle ID  + } rules={[ + { required: false, message: 'Please enter the bundle ID' }, { - required: form.getFieldValue(['apns', 'enabled']), - message: 'Please enter app bundle identifier when APNs is enabled' + pattern: /^[a-zA-Z0-9.-]+$/, + message: 'Invalid bundle ID format' } ]} > } /> - - - Production Mode  - - - - - } - valuePropName="checked" - > - - {/* FCM Configuration */} @@ -365,7 +321,7 @@ const PushNotificationPanel: React.FC = () => { size="small" > Enable FCM  @@ -380,41 +336,25 @@ const PushNotificationPanel: React.FC = () => { - FCM Credentials File Path  - + FCM Service Account Key Path  + } rules={[ - { - required: form.getFieldValue(['fcm', 'enabled']), - message: 'Please enter FCM credentials file path when FCM is enabled' - } + { required: false, message: 'Please enter the FCM credentials file path' } ]} > } /> - - -

- Note: Push notification settings require appropriate credentials and configuration files to function properly. Ensure you have the necessary APNs or FCM credentials configured on your server. -

-
); diff --git a/src/hooks/useGenericSettings.ts b/src/hooks/useGenericSettings.ts index 980c7332..a7a24f0d 100644 --- a/src/hooks/useGenericSettings.ts +++ b/src/hooks/useGenericSettings.ts @@ -204,6 +204,76 @@ const extractSettingsForGroup = (settings: any, groupName: string) => { return processedData; } + // Handle push notifications field name mapping + if (groupName === 'push_notifications' && rawData) { + const processedData: any = {}; + + // Copy top-level fields directly + if (rawData.enabled !== undefined) { + processedData.enabled = rawData.enabled; + } + + // Flatten service fields + if (rawData.service && typeof rawData.service === 'object') { + const serviceData = rawData.service; + if (serviceData.worker_count !== undefined) { + processedData.service_worker_count = serviceData.worker_count; + } + if (serviceData.queue_size !== undefined) { + processedData.service_queue_size = serviceData.queue_size; + } + if (serviceData.retry_attempts !== undefined) { + processedData.service_retry_attempts = serviceData.retry_attempts; + } + if (serviceData.retry_delay !== undefined) { + processedData.service_retry_delay = serviceData.retry_delay; + } + if (serviceData.batch_size !== undefined) { + processedData.service_batch_size = serviceData.batch_size; + } + } + + // Flatten APNs fields + if (rawData.apns && typeof rawData.apns === 'object') { + const apnsData = rawData.apns; + if (apnsData.enabled !== undefined) { + processedData.apns_enabled = apnsData.enabled; + } + if (apnsData.key_path !== undefined) { + processedData.apns_key_path = apnsData.key_path; + } + if (apnsData.bundle_id !== undefined) { + processedData.apns_bundle_id = apnsData.bundle_id; + } + if (apnsData.key_id !== undefined) { + processedData.apns_key_id = apnsData.key_id; + } + if (apnsData.team_id !== undefined) { + processedData.apns_team_id = apnsData.team_id; + } + if (apnsData.production !== undefined) { + processedData.apns_production = apnsData.production; + } + } + + // Flatten FCM fields + if (rawData.fcm && typeof rawData.fcm === 'object') { + const fcmData = rawData.fcm; + if (fcmData.enabled !== undefined) { + processedData.fcm_enabled = fcmData.enabled; + } + if (fcmData.credentials_path !== undefined) { + processedData.fcm_credentials_path = fcmData.credentials_path; + } + if (fcmData.project_id !== undefined) { + processedData.fcm_project_id = fcmData.project_id; + } + } + + console.log(`Processed ${groupName} data:`, processedData); + return processedData; + } + return rawData; }; @@ -336,9 +406,70 @@ const buildNestedUpdate = (groupName: string, data: any) => { return result; case 'push_notifications': + // Transform flat field names back to nested structure for backend + const backendPushData: any = {}; + + // Handle top-level fields + if (data.enabled !== undefined) { + backendPushData.enabled = data.enabled; + } + + // Handle service fields - group them under service object + const serviceFields = ['service_worker_count', 'service_queue_size', 'service_retry_attempts', 'service_retry_delay', 'service_batch_size']; + const serviceData: any = {}; + let hasServiceFields = false; + + serviceFields.forEach(flatFieldName => { + if (data[flatFieldName] !== undefined) { + const backendFieldName = flatFieldName.replace('service_', ''); + serviceData[backendFieldName] = data[flatFieldName]; + hasServiceFields = true; + } + }); + + if (hasServiceFields) { + backendPushData.service = serviceData; + } + + // Handle APNs fields - group them under apns object + const apnsFields = ['apns_enabled', 'apns_key_path', 'apns_bundle_id', 'apns_key_id', 'apns_team_id', 'apns_production']; + const apnsData: any = {}; + let hasApnsFields = false; + + apnsFields.forEach(flatFieldName => { + if (data[flatFieldName] !== undefined) { + const backendFieldName = flatFieldName.replace('apns_', ''); + apnsData[backendFieldName] = data[flatFieldName]; + hasApnsFields = true; + } + }); + + if (hasApnsFields) { + backendPushData.apns = apnsData; + } + + // Handle FCM fields - group them under fcm object + const fcmFields = ['fcm_enabled', 'fcm_credentials_path', 'fcm_project_id']; + const fcmData: any = {}; + let hasFcmFields = false; + + fcmFields.forEach(flatFieldName => { + if (data[flatFieldName] !== undefined) { + const backendFieldName = flatFieldName.replace('fcm_', ''); + fcmData[backendFieldName] = data[flatFieldName]; + hasFcmFields = true; + } + }); + + if (hasFcmFields) { + backendPushData.fcm = fcmData; + } + + console.log('Push notifications: transforming flat data to nested for backend:', { flatData: data, nestedData: backendPushData }); + return { settings: { - push_notifications: data + push_notifications: backendPushData } }; diff --git a/src/types/settings.types.ts b/src/types/settings.types.ts index 503b1410..d02faaef 100644 --- a/src/types/settings.types.ts +++ b/src/types/settings.types.ts @@ -92,20 +92,20 @@ export interface PushNotificationSettings { service: { worker_count: number; queue_size: number; - retry_max_attempts: number; - retry_base_delay: string; + retry_attempts: number; + retry_delay: string; }; apns: { enabled: boolean; - key_file: string; + key_path: string; key_id: string; team_id: string; - topic: string; + bundle_id: string; production: boolean; }; fcm: { enabled: boolean; - credentials_file: string; + credentials_path: string; }; } From 9ea9f21a9b6bdf077752e27c305ea19488472c43 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Mon, 18 Aug 2025 13:21:18 +0200 Subject: [PATCH 6/7] feat: add kind 1809 (Audio Transcription Repost) to registered kinds - Add kind 1809 to noteOptions in relaySettings.ts - Add translation for kind1809 - Will appear as clickable checkbox in Event Kinds Configuration - Positioned after kind 1808 as they are related - Backend handler already exists for this registered kind --- src/constants/relaySettings.ts | 1 + src/locales/en/translation.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/constants/relaySettings.ts b/src/constants/relaySettings.ts index 3370236f..26778ff3 100644 --- a/src/constants/relaySettings.ts +++ b/src/constants/relaySettings.ts @@ -39,6 +39,7 @@ export const noteOptions = [ { kind: 1060, kindString: 'kind1060', description: 'Double Ratchet DM', category: 1 }, { kind: 1063, kindString: 'kind1063', description: 'File Metadata', category: 1 }, { kind: 1808, kindString: 'kind1808', description: 'Audio Transcription', category: 1 }, + { kind: 1809, kindString: 'kind1809', description: 'Audio Transcription Repost', category: 1 }, { kind: 1984, kindString: 'kind1984', description: 'Reporting', category: 1 }, { kind: 30000, kindString: 'kind30000', description: 'Custom Follow List', category: 1 }, { kind: 30008, kindString: 'kind30008', description: 'Profile Badge', category: 2 }, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 979ca21f..a9b87a27 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -140,6 +140,7 @@ "kind1": "Kind 1", "kind10000": "Kind 10000", "kind1808": "Kind 1808", + "kind1809": "Kind 1809", "kind1984": "Kind 1984", "kind3": "Kind 3", "kind30000": "Kind 30000", From f1041726cb2cb752eea3190d2367b76480f7a2e3 Mon Sep 17 00:00:00 2001 From: whosthatguy <28675414+whosthatguy@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:59:36 -0700 Subject: [PATCH 7/7] Update Push Notification settings to use LiquidToggle components for consistency --- .../settings/PushNotificationSettings.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/settings/PushNotificationSettings.tsx b/src/components/settings/PushNotificationSettings.tsx index d6912100..0f3c8037 100644 --- a/src/components/settings/PushNotificationSettings.tsx +++ b/src/components/settings/PushNotificationSettings.tsx @@ -1,14 +1,15 @@ import React, { useEffect, useState } from 'react'; -import { Form, Input, InputNumber, Switch, Tooltip, Card, Divider } from 'antd'; -import { - QuestionCircleOutlined, - BellOutlined, - AppleOutlined, - AndroidOutlined, +import { Form, Input, InputNumber, Tooltip, Card } from 'antd'; +import { + QuestionCircleOutlined, + BellOutlined, + AppleOutlined, + AndroidOutlined, SettingOutlined, KeyOutlined, FileTextOutlined } from '@ant-design/icons'; +import { LiquidToggle } from '@app/components/common/LiquidToggle'; import useGenericSettings from '@app/hooks/useGenericSettings'; import { SettingsGroupType } from '@app/types/settings.types'; import BaseSettingsForm from './BaseSettingsForm'; @@ -98,7 +99,7 @@ const PushNotificationSettings: React.FC = () => { } valuePropName="checked" > - +
@@ -251,7 +252,7 @@ const PushNotificationSettings: React.FC = () => { } valuePropName="checked" > - + { } valuePropName="checked" > - + @@ -388,7 +389,7 @@ const PushNotificationSettings: React.FC = () => { } valuePropName="checked" > - +