diff --git a/README.md b/README.md index c445a521..92350774 100644 --- a/README.md +++ b/README.md @@ -209,8 +209,43 @@ server { proxy_pass http://wallet_service; } + # Blossom file storage routes + location /blossom/ { + proxy_pass http://panel_service; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Disable buffering for file uploads/downloads + proxy_buffering off; + proxy_request_buffering off; + + # Set appropriate headers + proxy_set_header Accept-Encoding ""; + + # Larger timeouts for file operations + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_connect_timeout 60s; + } + # Default location - Panel service (frontend + API) - MUST BE LAST location / { + # Add CORS headers for the panel service + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; + + # Handle preflight OPTIONS requests + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization'; + add_header 'Content-Length' 0; + return 204; + } + proxy_pass http://panel_service; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/fixed_nginx_config.conf b/fixed_nginx_config.conf index 35e0cdae..85e245de 100644 --- a/fixed_nginx_config.conf +++ b/fixed_nginx_config.conf @@ -85,8 +85,43 @@ server { proxy_pass http://wallet_service; } + # Blossom file storage routes + location /blossom/ { + proxy_pass http://panel_service; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Disable buffering for file uploads/downloads + proxy_buffering off; + proxy_request_buffering off; + + # Set appropriate headers + proxy_set_header Accept-Encoding ""; + + # Larger timeouts for file operations + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_connect_timeout 60s; + } + # Default location - Panel service (frontend + API) - MUST BE LAST location / { + # Add CORS headers for the panel service + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; + + # Handle preflight OPTIONS requests + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization'; + add_header 'Content-Length' 0; + return 204; + } + proxy_pass http://panel_service; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -98,4 +133,4 @@ server { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } -} \ No newline at end of file +} diff --git a/src/components/relay-settings/layouts/DesktopLayout.tsx b/src/components/relay-settings/layouts/DesktopLayout.tsx index 52ffc833..5faf702d 100644 --- a/src/components/relay-settings/layouts/DesktopLayout.tsx +++ b/src/components/relay-settings/layouts/DesktopLayout.tsx @@ -12,11 +12,14 @@ import { NetworkSection } from '@app/components/relay-settings/sections/NetworkS import { KindsSection } from '@app/components/relay-settings/sections/KindsSection'; import { MediaSection } from '@app/components/relay-settings/sections/MediaSection'; import { ModerationSection } from '@app/components/relay-settings/sections/ModerationSection'; +import { CollapsibleSection } from '@app/components/relay-settings/shared/CollapsibleSection/CollapsibleSection'; +import { BaseSwitch } from '@app/components/common/BaseSwitch/BaseSwitch'; import { useTranslation } from 'react-i18next'; interface DesktopLayoutProps { - mode: string; - onModeChange: (checked: boolean) => void; + allowUnregisteredKinds: boolean; + registeredKinds: number[]; + onAllowUnregisteredKindsChange: (allowed: boolean) => void; onSaveClick: () => void; loadings: boolean[]; // Network section props @@ -65,8 +68,9 @@ interface DesktopLayoutProps { } export const DesktopLayout: React.FC = ({ - mode, - onModeChange, + allowUnregisteredKinds, + registeredKinds, + onAllowUnregisteredKindsChange, onSaveClick, loadings, // Network props @@ -120,28 +124,46 @@ export const DesktopLayout: React.FC = ({ - - {t('common.serverSetting')} - - + + + +
+
+

+ {t('common.allowUnregisteredKinds')} +

+

+ Enable this to allow events with kind numbers that don't have specific handlers in the relay. +

+ {allowUnregisteredKinds && ( +
+ + ⚠️ + {t('common.allowUnregisteredKindsWarning')} + +
+ )} +
+ +
+
+
+
= ({ /> void; + allowUnregisteredKinds: boolean; + registeredKinds: number[]; + onAllowUnregisteredKindsChange: (allowed: boolean) => void; onSaveClick: () => void; loadings: boolean[]; // Network section props @@ -62,8 +65,9 @@ interface MobileLayoutProps { } export const MobileLayout: React.FC = ({ - mode, - onModeChange, + allowUnregisteredKinds, + registeredKinds, + onAllowUnregisteredKindsChange, onSaveClick, loadings, // Network props @@ -111,27 +115,49 @@ export const MobileLayout: React.FC = ({ onModerationModeChange={onModerationModeChange} /> - - {t('common.serverSetting')} - - + + + +
+
+

+ {t('common.allowUnregisteredKinds')} +

+

+ Enable this to allow events with kind numbers that don't have specific handlers in the relay. +

+
+ +
+ +
+ + {allowUnregisteredKinds && ( +
+ + ⚠️ + {t('common.allowUnregisteredKindsWarning')} + +
+ )} +
+
+
+
= ({ /> = ({ - mode, + allowUnregisteredKinds, + registeredKinds, isKindsActive, selectedKinds, dynamicKinds, @@ -33,37 +35,34 @@ export const KindsSection: React.FC = ({ onAddKind, onRemoveKind, }) => { - const header = mode !== 'whitelist' ? 'Blacklisted Kind Numbers' : 'Kind Numbers'; + const header = 'Event Kinds Configuration'; return (
- {mode !== 'blacklist' && mode !== '' && ( -
- onKindsActiveChange(!isKindsActive)} - /> -
- )} +
+ onKindsActiveChange(!isKindsActive)} + /> +
- void; - mode: string; } -export const AddKindForm: React.FC = ({ onAddKind, mode }) => { +export const AddKindForm: React.FC = ({ onAddKind }) => { const [newKind, setNewKind] = useState(''); const handleAddKind = () => { @@ -23,7 +22,7 @@ export const AddKindForm: React.FC = ({ onAddKind, mode }) => return (
-

{mode === 'blacklist' ? 'Add Custom Kind to Whitelist' : 'Add Custom Kind'}

+

Add Custom Kind

void; onRemoveKind: (kind: string) => void; - mode: string; } export const DynamicKindsList: React.FC = ({ + allowUnregisteredKinds, + registeredKinds, dynamicKinds, selectedDynamicKinds, onDynamicKindsChange, onRemoveKind, - mode, }) => { if (!dynamicKinds.length) { return null; @@ -29,38 +31,69 @@ export const DynamicKindsList: React.FC = ({ onDynamicKindsChange(checkedValues as string[]); }; + // Helper to extract kind number from string like "kind12345" + const getKindNumber = (kindStr: string): number => { + return parseInt(kindStr.replace('kind', ''), 10); + }; + + // Check if a dynamic kind is registered + const isDynamicKindRegistered = (kindStr: string): boolean => { + const kindNumber = getKindNumber(kindStr); + return registeredKinds.includes(kindNumber); + }; + return ( - {dynamicKinds.map((kind) => ( -
-
- - { + const isRegistered = isDynamicKindRegistered(kind); + const isSelected = selectedDynamicKinds.includes(kind); + + // Show status based on registration and selection + let statusIcon; + if (isSelected) { + statusIcon = isRegistered ? '✅' : '⚠️'; // Selected: green check for registered, warning for unregistered + } else { + statusIcon = '❌'; // Not selected: red X + } + + return ( +
+ {statusIcon} +
+ + + {kind} + {!isRegistered && (Unregistered)} + +
+ onRemoveKind(kind)} > - {kind} - + Remove +
- onRemoveKind(kind)} - > - Remove - -
- ))} + ); + })} ); }; diff --git a/src/components/relay-settings/sections/KindsSection/components/KindsList.tsx b/src/components/relay-settings/sections/KindsSection/components/KindsList.tsx index ae7951cb..177fab1a 100644 --- a/src/components/relay-settings/sections/KindsSection/components/KindsList.tsx +++ b/src/components/relay-settings/sections/KindsSection/components/KindsList.tsx @@ -9,14 +9,12 @@ import { useAppSelector } from '@app/hooks/reduxHooks'; import { useTranslation } from 'react-i18next'; interface KindsListProps { - mode: string; selectedKinds: string[]; isKindsActive: boolean; onKindsChange: (values: string[]) => void; } export const KindsList: React.FC = ({ - mode, selectedKinds, isKindsActive, onKindsChange, @@ -29,37 +27,56 @@ export const KindsList: React.FC = ({ notes: noteOptions.filter((note) => note.category === category.id), })); + // All kinds in our list are registered kinds (we know about them) + // The checkbox state determines if they're enabled or disabled + return ( onKindsChange(checkedValues as string[])} - disabled={mode !== 'whitelist' ? false : !isKindsActive} + disabled={!isKindsActive} > {groupedNoteOptions.map((group) => (

{group.name}

- {group.notes.map((note) => ( -
- - { + const isSelected = selectedKinds.includes(note.kindString); + const statusIcon = isSelected ? '✅' : '❌'; + + return ( +
- {t(`kind${note.kind}`)} - {' '} - {note.description} - -
- ))} + {statusIcon} + + + {t(`kind${note.kind}`)} - {' '} + + {note.description} + + +
+ ); + })}
))} diff --git a/src/components/relay-settings/sections/MediaSection/MediaSection.tsx b/src/components/relay-settings/sections/MediaSection/MediaSection.tsx index b365ebde..94b3ac7c 100644 --- a/src/components/relay-settings/sections/MediaSection/MediaSection.tsx +++ b/src/components/relay-settings/sections/MediaSection/MediaSection.tsx @@ -8,7 +8,6 @@ import { MediaToggle } from './components/MediaToggle'; import { FileSizeLimitInput } from './components/FileSizeLimitInput'; export interface MediaSectionProps { - mode: string; photos: { selected: string[]; isActive: boolean; @@ -77,29 +76,23 @@ const audioFormats = [ ]; export const MediaSection: React.FC = ({ - mode, photos, videos, audio, }) => { - const getHeader = (type: string) => - mode !== 'whitelist' ? `Blacklisted ${type} Extensions` : `${type} Extensions`; - return ( <> - + = ({ - + = ({ - + void; - mode: string; } export const MediaToggle: React.FC = ({ isActive, onChange, - mode, }) => { - if (mode === 'blacklist') { - return null; - } - return (
void; isActive: boolean; - mode: string; } export const MediaTypeList: React.FC = ({ @@ -24,31 +23,38 @@ export const MediaTypeList: React.FC = ({ selectedFormats, onChange, isActive, - mode, }) => { const theme = useAppSelector((state) => state.theme.theme); - const options = formats.map((format) => ({ - label: ( - - {format.ext.toUpperCase()} - - ), - value: format.mime - })); + const options = formats.map((format) => { + const isSelected = selectedFormats.includes(format.mime); + const statusIcon = isSelected ? '✅' : '❌'; + + return { + label: ( +
+ {statusIcon} + + {format.ext.toUpperCase()} + +
+ ), + value: format.mime + }; + }); return ( onChange(checkedValues as string[])} - disabled={mode !== 'whitelist' ? false : !isActive} + disabled={!isActive} /> ); }; diff --git a/src/components/relay-settings/sections/NetworkSection/components/ProtocolSelect.tsx b/src/components/relay-settings/sections/NetworkSection/components/ProtocolSelect.tsx index 6228b550..58c25855 100644 --- a/src/components/relay-settings/sections/NetworkSection/components/ProtocolSelect.tsx +++ b/src/components/relay-settings/sections/NetworkSection/components/ProtocolSelect.tsx @@ -23,8 +23,28 @@ export const ProtocolSelect: React.FC = ({ + + {selectedProtocols.includes('WebSocket') ? '✅' : '❌'} + + WebSocket +
+ ), + value: 'WebSocket' + }, + { + label: ( +
+ + {selectedProtocols.includes('QUIC') ? '✅' : '❌'} + + Libp2p QUIC +
+ ), + value: 'QUIC' + }, ]} value={selectedProtocols} className="custom-checkbox-group" diff --git a/src/constants/relaySettings.ts b/src/constants/relaySettings.ts index 4ad824cb..3370236f 100644 --- a/src/constants/relaySettings.ts +++ b/src/constants/relaySettings.ts @@ -1,5 +1,7 @@ export type Settings = { - mode: string; + // Event filtering settings + allowUnregisteredKinds: boolean; // Controls whether unregistered kinds are accepted + registeredKinds: number[]; // List of all kinds with specific handlers (from backend) protocol: string[]; kinds: string[]; dynamicKinds: string[]; @@ -36,6 +38,7 @@ export const noteOptions = [ { kind: 10002, kindString: 'kind10002', description: 'Tiny Relay List', category: 1 }, { 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: 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/hooks/useRelaySettings.ts b/src/hooks/useRelaySettings.ts index 4ff8f7e1..3567eed1 100644 --- a/src/hooks/useRelaySettings.ts +++ b/src/hooks/useRelaySettings.ts @@ -1,15 +1,13 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useCallback } from 'react'; import config from '@app/config/config'; import { readToken } from '@app/services/localStorage.service'; import { useHandleLogout } from './authUtils'; import { Settings } from '@app/constants/relaySettings'; -import { CORE_KINDS, ensureCoreKinds, calculateInverseKinds, getAllPossibleKinds, isCoreKind, getAllPossibleMediaTypes, calculateInverseMediaTypes } from '@app/constants/coreKinds'; - -// Legacy interface - no longer used with new API -// interface BackendRelaySettings { ... } +import { CORE_KINDS, ensureCoreKinds } from '@app/constants/coreKinds'; const getInitialSettings = (): Settings => ({ - mode: 'whitelist', + allowUnregisteredKinds: false, // Default to strict mode + registeredKinds: [], // Will be populated from backend protocol: ['WebSocket'], kinds: [...CORE_KINDS], // Always start with core kinds dynamicKinds: [], @@ -32,161 +30,52 @@ const getInitialSettings = (): Settings => ({ const useRelaySettings = () => { const [relaySettings, setRelaySettings] = useState(getInitialSettings()); - const [previousSmartSettings, setPreviousSmartSettings] = useState<{ - kinds: string[]; - photos: string[]; - videos: string[]; - audio: string[]; - } | null>(null); const handleLogout = useHandleLogout(); const token = readToken(); - - // Keep track of the last mode to prevent unnecessary updates - const lastMode = useRef(null); - - /* eslint-disable react-hooks/exhaustive-deps */ - // Effect to handle mode changes - only for manual user mode switches, not initial load - useEffect(() => { - // Skip if this is the initial load (lastMode is undefined/null) - if (lastMode.current === undefined || lastMode.current === null) { - lastMode.current = relaySettings.mode; - return; - } - // Skip if mode hasn't actually changed - if (relaySettings.mode === lastMode.current) { - return; - } - - console.log(`[useRelaySettings] Mode change detected: ${lastMode.current} -> ${relaySettings.mode}`); - - // When user manually switches TO blacklist mode from whitelist mode - if (relaySettings.mode === 'blacklist' && lastMode.current === 'whitelist') { - // Store current whitelist settings before clearing - setPreviousSmartSettings({ - kinds: relaySettings.kinds, - photos: relaySettings.photos, - videos: relaySettings.videos, - audio: relaySettings.audio, - }); - - // Always clear selections when switching to blacklist mode - user starts fresh - setRelaySettings(prev => ({ - ...prev, - kinds: [], - photos: [], - videos: [], - audio: [], - })); - } else if (relaySettings.mode === 'whitelist' && lastMode.current === 'blacklist' && previousSmartSettings) { - // Restore previous whitelist mode settings - setRelaySettings(prev => ({ - ...prev, - kinds: previousSmartSettings.kinds, - photos: previousSmartSettings.photos, - videos: previousSmartSettings.videos, - audio: previousSmartSettings.audio, - })); - } - - // Update lastMode after processing - lastMode.current = relaySettings.mode; - }, [relaySettings.mode, previousSmartSettings]); - /* eslint-enable react-hooks/exhaustive-deps */ - - // Legacy transformation functions - kept for reference but not used with new API - // These can be removed once migration is fully tested - - // Simplified transformation functions based on actual backend response + // Transformation function for backend response const transformFromBackendSettings = useCallback((backendData: any): Settings => { console.log('Raw backend settings:', backendData); const settings = getInitialSettings(); - // Set the mode first to avoid triggering mode change logic during initial load - if (backendData.event_filtering?.mode) { - console.log(`[useRelaySettings] Setting lastMode to ${backendData.event_filtering.mode} during data load`); - lastMode.current = backendData.event_filtering.mode; - } - // Map from actual backend structure if (backendData.event_filtering) { - settings.mode = backendData.event_filtering.mode || 'whitelist'; + // New fields + settings.allowUnregisteredKinds = backendData.event_filtering.allow_unregistered_kinds || false; + settings.registeredKinds = backendData.event_filtering.registered_kinds || []; settings.moderationMode = backendData.event_filtering.moderation_mode || 'strict'; - // Handle kinds based on mode + + // Handle kinds - now simply extract from kind_whitelist const backendKinds = backendData.event_filtering.kind_whitelist || []; // Get all stored dynamic kinds from localStorage const allStoredDynamicKinds = JSON.parse(localStorage.getItem('dynamicKinds') || '[]'); - if (settings.mode === 'blacklist') { - // In blacklist mode: backend sends allowed kinds, we need to calculate blocked kinds - const allPossibleKinds = getAllPossibleKinds(); - const backendKindsSet = new Set(backendKinds); - const allKindsSet = new Set(allPossibleKinds); - - // Calculate blocked predefined kinds: all possible kinds minus backend allowed kinds - const blockedPredefinedKinds = Array.from(allKindsSet).filter(kind => !backendKindsSet.has(kind)); - // Only show non-core kinds as blocked (core kinds can't be blocked) - settings.kinds = blockedPredefinedKinds.filter(kind => !isCoreKind(kind)); - - // Calculate blocked dynamic kinds: stored dynamic kinds that are NOT in backend allowed kinds - settings.dynamicKinds = allStoredDynamicKinds.filter((kind: string) => !backendKindsSet.has(kind)); - - console.log('[useRelaySettings] Blacklist mode - Backend allowed kinds:', backendKinds); - console.log('[useRelaySettings] Blacklist mode - Calculated blocked predefined kinds:', settings.kinds); - console.log('[useRelaySettings] Blacklist mode - Calculated blocked dynamic kinds:', settings.dynamicKinds); - } else { - // In whitelist mode: backend sends allowed kinds directly - // Separate predefined kinds from dynamic kinds - const allPossibleKinds = getAllPossibleKinds(); - const predefinedKinds = backendKinds.filter((kind: string) => allPossibleKinds.includes(kind)); - const dynamicKinds = backendKinds.filter((kind: string) => !allPossibleKinds.includes(kind) && allStoredDynamicKinds.includes(kind)); - - settings.kinds = ensureCoreKinds(predefinedKinds); - settings.dynamicKinds = dynamicKinds; - } + // Simply separate kinds into registered (predefined) and dynamic + settings.kinds = ensureCoreKinds(backendKinds.filter((kind: string) => + settings.registeredKinds.some((rk: number) => `kind${rk}` === kind) + )); + settings.dynamicKinds = backendKinds.filter((kind: string) => + !settings.registeredKinds.some((rk: number) => `kind${rk}` === kind) && + allStoredDynamicKinds.includes(kind) + ); // Extract mime types and file sizes from actual backend format const mediaDefinitions = backendData.event_filtering.media_definitions || {}; - // Handle both old and new field names for backward compatibility - const backendPhotos = mediaDefinitions.image?.mime_patterns || mediaDefinitions.image?.mimepatterns || []; - const backendVideos = mediaDefinitions.video?.mime_patterns || mediaDefinitions.video?.mimepatterns || []; - const backendAudio = mediaDefinitions.audio?.mime_patterns || mediaDefinitions.audio?.mimepatterns || []; + const backendPhotos = mediaDefinitions.image?.mime_patterns || []; + const backendVideos = mediaDefinitions.video?.mime_patterns || []; + const backendAudio = mediaDefinitions.audio?.mime_patterns || []; - // Handle media types based on mode (same logic as kinds) - if (settings.mode === 'blacklist') { - // In blacklist mode: backend sends allowed media types, we need to calculate blocked types - const allPossiblePhotos = getAllPossibleMediaTypes('photos'); - const allPossibleVideos = getAllPossibleMediaTypes('videos'); - const allPossibleAudio = getAllPossibleMediaTypes('audio'); - - // Use Sets for more reliable comparison - const backendPhotosSet = new Set(backendPhotos); - const backendVideosSet = new Set(backendVideos); - const backendAudioSet = new Set(backendAudio); - - settings.photos = allPossiblePhotos.filter(type => !backendPhotosSet.has(type)); - settings.videos = allPossibleVideos.filter(type => !backendVideosSet.has(type)); - settings.audio = allPossibleAudio.filter(type => !backendAudioSet.has(type)); - - console.log('[useRelaySettings] Blacklist mode - Backend allowed photos:', backendPhotos); - console.log('[useRelaySettings] Blacklist mode - Calculated blocked photos:', settings.photos); - console.log('[useRelaySettings] Blacklist mode - Backend allowed videos:', backendVideos); - console.log('[useRelaySettings] Blacklist mode - Calculated blocked videos:', settings.videos); - console.log('[useRelaySettings] Blacklist mode - Backend allowed audio:', backendAudio); - console.log('[useRelaySettings] Blacklist mode - Calculated blocked audio:', settings.audio); - } else { - // In whitelist mode: backend sends allowed media types directly - settings.photos = backendPhotos; - settings.videos = backendVideos; - settings.audio = backendAudio; - } + // Media types are now directly from backend + settings.photos = backendPhotos; + settings.videos = backendVideos; + settings.audio = backendAudio; - // Extract file size limits (handle both old and new field names) - settings.photoMaxSizeMB = mediaDefinitions.image?.max_size_mb || mediaDefinitions.image?.maxsizemb || 100; - settings.videoMaxSizeMB = mediaDefinitions.video?.max_size_mb || mediaDefinitions.video?.maxsizemb || 500; - settings.audioMaxSizeMB = mediaDefinitions.audio?.max_size_mb || mediaDefinitions.audio?.maxsizemb || 100; + // Extract file size limits + settings.photoMaxSizeMB = mediaDefinitions.image?.max_size_mb || 100; + settings.videoMaxSizeMB = mediaDefinitions.video?.max_size_mb || 500; + settings.audioMaxSizeMB = mediaDefinitions.audio?.max_size_mb || 100; // Set protocols if (backendData.event_filtering.protocols?.enabled) { @@ -196,17 +85,6 @@ const useRelaySettings = () => { } } - // Store these as the previous whitelist settings ONLY if in whitelist mode - // In blacklist mode, settings.kinds/photos/etc contain blocked items, not allowed items - if (settings.mode === 'whitelist') { - setPreviousSmartSettings({ - kinds: settings.kinds, - photos: settings.photos, - videos: settings.videos, - audio: settings.audio, - }); - } - // Set active states settings.isKindsActive = true; settings.isPhotosActive = true; @@ -218,57 +96,31 @@ const useRelaySettings = () => { }, []); const transformToBackendSettings = useCallback((settings: Settings) => { - // Handle media types based on mode - same logic as kinds - const photoMimePatterns = settings.mode === 'blacklist' - ? calculateInverseMediaTypes(settings.photos, 'photos') // For blacklist: send inverse (all types except blocked ones) - : settings.photos; // For whitelist: send selected types directly - - const videoMimePatterns = settings.mode === 'blacklist' - ? calculateInverseMediaTypes(settings.videos, 'videos') // For blacklist: send inverse (all types except blocked ones) - : settings.videos; // For whitelist: send selected types directly - - const audioMimePatterns = settings.mode === 'blacklist' - ? calculateInverseMediaTypes(settings.audio, 'audio') // For blacklist: send inverse (all types except blocked ones) - : settings.audio; // For whitelist: send selected types directly - - // Always create media definitions with correct field names to avoid backend conflicts + // Create media definitions const mediaDefinitions = { image: { - mime_patterns: photoMimePatterns, // Send processed mime patterns based on mode + mime_patterns: settings.photos, extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"], - max_size_mb: settings.photoMaxSizeMB // Only send correct field name + max_size_mb: settings.photoMaxSizeMB }, video: { - mime_patterns: videoMimePatterns, // Send processed mime patterns based on mode + mime_patterns: settings.videos, extensions: [".mp4", ".webm", ".avi", ".mov"], - max_size_mb: settings.videoMaxSizeMB // Only send correct field name + max_size_mb: settings.videoMaxSizeMB }, audio: { - mime_patterns: audioMimePatterns, // Send processed mime patterns based on mode + mime_patterns: settings.audio, extensions: [".mp3", ".wav", ".ogg", ".flac"], - max_size_mb: settings.audioMaxSizeMB // Only send correct field name + max_size_mb: settings.audioMaxSizeMB } }; return { settings: { event_filtering: { - mode: settings.mode, + allow_unregistered_kinds: settings.allowUnregisteredKinds, moderation_mode: settings.moderationMode, - kind_whitelist: (() => { - if (settings.mode === 'blacklist') { - // For blacklist: get all predefined allowed kinds, then add unblocked dynamic kinds - const predefinedAllowed = calculateInverseKinds(settings.kinds); - // Get all stored dynamic kinds from localStorage - const allStoredDynamicKinds = JSON.parse(localStorage.getItem('dynamicKinds') || '[]'); - // Add dynamic kinds that are NOT blocked (not in settings.dynamicKinds) - const allowedDynamicKinds = allStoredDynamicKinds.filter((kind: string) => !settings.dynamicKinds.includes(kind)); - return [...predefinedAllowed, ...allowedDynamicKinds]; - } else { - // For whitelist: send all selected kinds (including dynamic) - return ensureCoreKinds([...settings.kinds, ...settings.dynamicKinds]); - } - })(), + kind_whitelist: ensureCoreKinds([...settings.kinds, ...settings.dynamicKinds]), media_definitions: mediaDefinitions, dynamic_kinds: { enabled: false, @@ -322,15 +174,6 @@ const useRelaySettings = () => { if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - // Update previous whitelist settings after successful save - if (relaySettings.mode === 'whitelist') { - setPreviousSmartSettings({ - kinds: relaySettings.kinds, - photos: relaySettings.photos, - videos: relaySettings.videos, - audio: relaySettings.audio, - }); - } } catch (error) { console.error('Error saving settings:', error); throw error; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index c4405b23..979ca21f 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -139,6 +139,7 @@ "kind0": "Kind 0", "kind1": "Kind 1", "kind10000": "Kind 10000", + "kind1808": "Kind 1808", "kind1984": "Kind 1984", "kind3": "Kind 3", "kind30000": "Kind 30000", @@ -324,6 +325,14 @@ "success": "Success", "supportedKindsAndMedia": "Supported", "unsupportedKindsAndMedia": "Unsupported", + "allowUnregisteredKinds": "Allow Unregistered Kind Numbers", + "allowUnregisteredKindsWarning": "Warning: This will allow events without specific handlers", + "registeredKind": "Registered Kind", + "unregisteredKind": "Unregistered Kind", + "kindEnabled": "Enabled", + "kindDisabled": "Disabled", + "kindAllowed": "Allowed (Unregistered)", + "kindBlocked": "Blocked", "chunkedSetting": "File Chunking", "surgeon": "Surgeon", "switch": "Switch", diff --git a/src/pages/RelaySettingsPage.tsx b/src/pages/RelaySettingsPage.tsx index 8f6b9454..bb69c242 100644 --- a/src/pages/RelaySettingsPage.tsx +++ b/src/pages/RelaySettingsPage.tsx @@ -3,9 +3,6 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { PageTitle } from '@app/components/common/PageTitle/PageTitle'; -import { useDispatch } from 'react-redux'; -import { useAppSelector } from '@app/hooks/reduxHooks'; -import { setMode } from '@app/store/slices/modeSlice'; import { useResponsive } from '@app/hooks/useResponsive'; import useRelaySettings from '@app/hooks/useRelaySettings'; import { DesktopLayout } from '@app/components/relay-settings/layouts/DesktopLayout'; @@ -14,10 +11,7 @@ import { Settings } from '@app/constants/relaySettings'; const RelaySettingsPage: React.FC = () => { const { t } = useTranslation(); - const dispatch = useDispatch(); const { isDesktop } = useResponsive(); - const theme = useAppSelector((state) => state.theme.theme); - const relaymode = useAppSelector((state) => state.mode.relayMode); const { relaySettings, fetchSettings, updateSettings, saveSettings } = useRelaySettings(); // Loading state @@ -25,7 +19,8 @@ const RelaySettingsPage: React.FC = () => { // Local state for settings const [settings, setSettings] = useState({ - mode: JSON.parse(localStorage.getItem('relaySettings') || '{}').mode || relaymode || 'blacklist', + allowUnregisteredKinds: false, + registeredKinds: [], protocol: ['WebSocket'], kinds: [], dynamicKinds: [], @@ -66,18 +61,12 @@ const RelaySettingsPage: React.FC = () => { } }, [relaySettings]); - const handleModeChange = (checked: boolean) => { - const newMode = checked ? 'whitelist' : 'blacklist'; + const handleAllowUnregisteredKindsChange = (allowed: boolean) => { setSettings(prev => ({ ...prev, - mode: newMode, - kinds: newMode === 'blacklist' ? [] : prev.kinds, - photos: newMode === 'blacklist' ? [] : prev.photos, - videos: newMode === 'blacklist' ? [] : prev.videos, - audio: newMode === 'blacklist' ? [] : prev.audio, + allowUnregisteredKinds: allowed })); - updateSettings('mode', newMode); - dispatch(setMode(newMode)); + updateSettings('allowUnregisteredKinds', allowed); }; const handleSaveClick = async () => { @@ -176,8 +165,9 @@ const RelaySettingsPage: React.FC = () => { }; const layoutProps = { - mode: settings.mode, - onModeChange: handleModeChange, + allowUnregisteredKinds: settings.allowUnregisteredKinds, + registeredKinds: settings.registeredKinds, + onAllowUnregisteredKindsChange: handleAllowUnregisteredKindsChange, onSaveClick: handleSaveClick, loadings, // Network props diff --git a/src/types/kindStatus.types.ts b/src/types/kindStatus.types.ts new file mode 100644 index 00000000..ed33a029 --- /dev/null +++ b/src/types/kindStatus.types.ts @@ -0,0 +1,29 @@ +// Types for managing kind status in the new system + +export type KindStatusType = 'enabled' | 'disabled' | 'allowed-unregistered' | 'blocked-unregistered'; + +export interface KindStatus { + icon: string; + status: KindStatusType; + statusText: string; + canToggle: boolean; + info?: string; +} + +export interface KindInfo { + kind: number; + kindString: string; + description: string; + isRegistered: boolean; + isEnabled: boolean; + status: KindStatus; +} + +// Helper function type definitions +export type IsRegisteredKindFunc = (kind: number, registeredKinds: number[]) => boolean; +export type IsKindEnabledFunc = (kind: string, kindWhitelist: string[]) => boolean; +export type GetKindStatusFunc = (kind: number, settings: { + allowUnregisteredKinds: boolean; + registeredKinds: number[]; + kinds: string[]; +}) => KindStatus; \ No newline at end of file diff --git a/src/types/newSettings.types.ts b/src/types/newSettings.types.ts index d4466c9d..18873a1d 100644 --- a/src/types/newSettings.types.ts +++ b/src/types/newSettings.types.ts @@ -44,7 +44,8 @@ export interface ProtocolsConfig { } export interface EventFilteringConfig { - mode: "whitelist" | "blacklist"; + allow_unregistered_kinds: boolean; // Controls whether unregistered kinds are accepted + registered_kinds: number[]; // List of all kinds with specific handlers (read-only from frontend) moderation_mode: "basic" | "strict" | "full"; kind_whitelist: string[]; media_definitions: Record; @@ -172,7 +173,8 @@ export const getDefaultSettings = (): Partial => ({ ] }, event_filtering: { - mode: "whitelist", + allow_unregistered_kinds: false, // Default to strict mode (only registered kinds) + registered_kinds: [], // Will be populated from backend moderation_mode: "strict", kind_whitelist: [], media_definitions: {