From 2124aa98b82918c0ef3f4e305e9f6d0860a5ad19 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Fri, 4 Jul 2025 13:56:08 +0200 Subject: [PATCH 1/3] Fix blacklist mode logic for event filtering - Fix backend API communication for blacklist mode - In blacklist mode: selected items are blocked, backend receives inverse (allowed items) - In whitelist mode: selected items are allowed, backend receives them directly - Fix mode switching to preserve blocked items when loading from backend - Add helper functions to calculate inverse kinds for blacklist mode - Ensure core kinds are never blockable in blacklist mode - Add AddKindForm component for dynamic kind addition --- .../relay-settings/layouts/DesktopLayout.tsx | 2 + .../relay-settings/layouts/MobileLayout.tsx | 2 + .../sections/KindsSection/KindsSection.tsx | 8 +++ .../KindsSection/components/AddKindForm.tsx | 43 ++++++++++++ src/constants/coreKinds.ts | 18 +++++ src/hooks/useRelaySettings.ts | 66 ++++++++++++------- 6 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 src/components/relay-settings/sections/KindsSection/components/AddKindForm.tsx diff --git a/src/components/relay-settings/layouts/DesktopLayout.tsx b/src/components/relay-settings/layouts/DesktopLayout.tsx index 726f277a..84579739 100644 --- a/src/components/relay-settings/layouts/DesktopLayout.tsx +++ b/src/components/relay-settings/layouts/DesktopLayout.tsx @@ -97,6 +97,7 @@ export const DesktopLayout: React.FC = ({ onKindsActiveChange, onKindsChange, onDynamicKindsChange, + onAddKind, onRemoveKind, // Media props photos, @@ -171,6 +172,7 @@ export const DesktopLayout: React.FC = ({ onKindsActiveChange={onKindsActiveChange} onKindsChange={onKindsChange} onDynamicKindsChange={onDynamicKindsChange} + onAddKind={onAddKind} onRemoveKind={onRemoveKind} /> diff --git a/src/components/relay-settings/layouts/MobileLayout.tsx b/src/components/relay-settings/layouts/MobileLayout.tsx index af7f9c0c..542242b4 100644 --- a/src/components/relay-settings/layouts/MobileLayout.tsx +++ b/src/components/relay-settings/layouts/MobileLayout.tsx @@ -94,6 +94,7 @@ export const MobileLayout: React.FC = ({ onKindsActiveChange, onKindsChange, onDynamicKindsChange, + onAddKind, onRemoveKind, // Media props photos, @@ -162,6 +163,7 @@ export const MobileLayout: React.FC = ({ onKindsActiveChange={onKindsActiveChange} onKindsChange={onKindsChange} onDynamicKindsChange={onDynamicKindsChange} + onAddKind={onAddKind} onRemoveKind={onRemoveKind} /> diff --git a/src/components/relay-settings/sections/KindsSection/KindsSection.tsx b/src/components/relay-settings/sections/KindsSection/KindsSection.tsx index 87e62334..7c7bfa07 100644 --- a/src/components/relay-settings/sections/KindsSection/KindsSection.tsx +++ b/src/components/relay-settings/sections/KindsSection/KindsSection.tsx @@ -5,6 +5,7 @@ import { BaseSwitch } from '@app/components/common/BaseSwitch/BaseSwitch'; import * as S from '@app/pages/uiComponentsPages/UIComponentsPage.styles'; import { CollapsibleSection } from '../../shared/CollapsibleSection/CollapsibleSection'; import { KindsList } from './components/KindsList'; +import { AddKindForm } from './components/AddKindForm'; import { DynamicKindsList } from './components/DynamicKindsList'; export interface KindsSectionProps { @@ -16,6 +17,7 @@ export interface KindsSectionProps { onKindsActiveChange: (active: boolean) => void; onKindsChange: (values: string[]) => void; onDynamicKindsChange: (values: string[]) => void; + onAddKind: (kind: string) => void; onRemoveKind: (kind: string) => void; } @@ -28,6 +30,7 @@ export const KindsSection: React.FC = ({ onKindsActiveChange, onKindsChange, onDynamicKindsChange, + onAddKind, onRemoveKind, }) => { const header = mode !== 'whitelist' ? 'Blacklisted Kind Numbers' : 'Kind Numbers'; @@ -54,6 +57,11 @@ export const KindsSection: React.FC = ({ onKindsChange={onKindsChange} /> + + void; + mode: string; +} + +export const AddKindForm: React.FC = ({ onAddKind, mode }) => { + const [newKind, setNewKind] = useState(''); + + const handleAddKind = () => { + if (newKind) { + onAddKind(newKind); + setNewKind(''); + } + }; + + if (mode === 'whitelist') { + return null; + } + + return ( +
+

{'Add to Blacklist'}

+
+ setNewKind(e.target.value)} + placeholder="Enter new kind" + /> + + Add Kind + +
+
+ ); +}; + +export default AddKindForm; \ No newline at end of file diff --git a/src/constants/coreKinds.ts b/src/constants/coreKinds.ts index 9fb173b7..c58df4d7 100644 --- a/src/constants/coreKinds.ts +++ b/src/constants/coreKinds.ts @@ -22,4 +22,22 @@ export const ensureCoreKinds = (kindList: string[]): string[] => { // Helper function to check if a kind is protected export const isCoreKind = (kind: string): boolean => { return CORE_KINDS.includes(kind); +}; + +// Helper function to get all possible kinds from noteOptions +export const getAllPossibleKinds = (): string[] => { + // Import noteOptions dynamically to avoid circular dependency + const { noteOptions } = require('../constants/relaySettings'); + return noteOptions.map((option: any) => option.kindString); +}; + +// Helper function to calculate inverse for blacklist mode +export const calculateInverseKinds = (selectedKinds: string[]): string[] => { + const allPossibleKinds = getAllPossibleKinds(); + // In blacklist mode: selected = blocked, so remove selected from all possible kinds + // Core kinds can never be blocked, so they're always in the whitelist + const allowedKinds = allPossibleKinds.filter(kind => + !selectedKinds.includes(kind) || isCoreKind(kind) + ); + return allowedKinds; }; \ No newline at end of file diff --git a/src/hooks/useRelaySettings.ts b/src/hooks/useRelaySettings.ts index 6f12db6b..2e9c61e3 100644 --- a/src/hooks/useRelaySettings.ts +++ b/src/hooks/useRelaySettings.ts @@ -3,7 +3,7 @@ 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 } from '@app/constants/coreKinds'; +import { CORE_KINDS, ensureCoreKinds, calculateInverseKinds, getAllPossibleKinds, isCoreKind } from '@app/constants/coreKinds'; // Legacy interface - no longer used with new API // interface BackendRelaySettings { ... } @@ -54,25 +54,32 @@ const useRelaySettings = () => { return; } - lastMode.current = relaySettings.mode; - - if (relaySettings.mode === 'blacklist') { - // Store current settings before clearing - setPreviousSmartSettings({ - kinds: relaySettings.kinds, - photos: relaySettings.photos, - videos: relaySettings.videos, - audio: relaySettings.audio, - }); + // Only clear arrays when user manually switches TO blacklist mode + // Don't clear if we're in blacklist mode and already have blocked items (loaded from backend) + if (relaySettings.mode === 'blacklist' && lastMode.current === 'whitelist') { + // Only clear if we don't have any blocked items (user switching, not loading from backend) + const hasBlockedItems = relaySettings.kinds.length > 0 || relaySettings.photos.length > 0 || + relaySettings.videos.length > 0 || relaySettings.audio.length > 0; + + if (!hasBlockedItems) { + // Store current settings before clearing + setPreviousSmartSettings({ + kinds: relaySettings.kinds, + photos: relaySettings.photos, + videos: relaySettings.videos, + audio: relaySettings.audio, + }); - setRelaySettings(prev => ({ - ...prev, - kinds: [], - photos: [], - videos: [], - audio: [], - })); - } else if (relaySettings.mode === 'whitelist' && previousSmartSettings) { + // Clear selections in blacklist mode - user starts fresh and selects what to block + setRelaySettings(prev => ({ + ...prev, + kinds: [], + photos: [], + videos: [], + audio: [], + })); + } + } else if (relaySettings.mode === 'whitelist' && lastMode.current === 'blacklist' && previousSmartSettings) { // Restore previous whitelist mode settings setRelaySettings(prev => ({ ...prev, @@ -82,6 +89,9 @@ const useRelaySettings = () => { audio: previousSmartSettings.audio, })); } + + // Update lastMode after processing + lastMode.current = relaySettings.mode; }, [relaySettings.mode, previousSmartSettings]); /* eslint-enable react-hooks/exhaustive-deps */ @@ -97,9 +107,19 @@ const useRelaySettings = () => { if (backendData.event_filtering) { settings.mode = backendData.event_filtering.mode || 'whitelist'; settings.moderationMode = backendData.event_filtering.moderation_mode || 'strict'; - // Always ensure core kinds are included from backend data + // Handle kinds based on mode const backendKinds = backendData.event_filtering.kind_whitelist || []; - settings.kinds = ensureCoreKinds(backendKinds); + + if (settings.mode === 'blacklist') { + // In blacklist mode: backend sends allowed kinds, we need to calculate blocked kinds + const allPossibleKinds = getAllPossibleKinds(); + const blockedKinds = allPossibleKinds.filter(kind => !backendKinds.includes(kind)); + // Only show non-core kinds as blocked (core kinds can't be blocked) + settings.kinds = blockedKinds.filter(kind => !isCoreKind(kind)); + } else { + // In whitelist mode: backend sends allowed kinds directly + settings.kinds = ensureCoreKinds(backendKinds); + } // Extract mime types and file sizes from actual backend format const mediaDefinitions = backendData.event_filtering.media_definitions || {}; @@ -166,7 +186,9 @@ const useRelaySettings = () => { event_filtering: { mode: settings.mode, moderation_mode: settings.moderationMode, - kind_whitelist: ensureCoreKinds(settings.kinds), // Always include core kinds + kind_whitelist: settings.mode === 'blacklist' + ? calculateInverseKinds(settings.kinds) // For blacklist: send inverse (all kinds except blocked ones) + : ensureCoreKinds(settings.kinds), // For whitelist: send selected kinds directly media_definitions: mediaDefinitions, dynamic_kinds: { enabled: false, From 83277c1a008c14c077fd45d3ee4406a416f23975 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Fri, 4 Jul 2025 22:00:17 +0200 Subject: [PATCH 2/3] Fix profile caching and blacklist mode reconciliation - Add 10-minute caching to subscriber profile fetching to prevent backend spam - Implement LRU cache with automatic cleanup for thousands of users - Fix blacklist mode reconciliation to properly show blocked items on page refresh - Use Set-based logic for more reliable blacklist calculations - Remove App Buckets functionality from relay settings - Clean up excessive console logging --- src/App.tsx | 2 +- .../paid-subscribers/PaidSubscribers.tsx | 233 +++++++++++++++--- .../relay-settings/layouts/DesktopLayout.tsx | 23 -- .../relay-settings/layouts/MobileLayout.tsx | 24 -- .../AppBucketsSection/AppBucketsSection.tsx | 62 ----- .../components/AddBucketForm.tsx | 39 --- .../components/BucketsList.tsx | 49 ---- .../components/DynamicBucketsList.tsx | 59 ----- .../sections/AppBucketsSection/index.ts | 4 - src/constants/coreKinds.ts | 32 ++- src/constants/relaySettings.ts | 15 +- src/hooks/usePaidSubscribers.ts | 67 +++-- src/hooks/useRelaySettings.ts | 128 +++++++--- src/pages/RelaySettingsPage.tsx | 74 +----- 14 files changed, 359 insertions(+), 452 deletions(-) delete mode 100644 src/components/relay-settings/sections/AppBucketsSection/AppBucketsSection.tsx delete mode 100644 src/components/relay-settings/sections/AppBucketsSection/components/AddBucketForm.tsx delete mode 100644 src/components/relay-settings/sections/AppBucketsSection/components/BucketsList.tsx delete mode 100644 src/components/relay-settings/sections/AppBucketsSection/components/DynamicBucketsList.tsx delete mode 100644 src/components/relay-settings/sections/AppBucketsSection/index.ts diff --git a/src/App.tsx b/src/App.tsx index 2c9bf942..b6173215 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import { usePWA } from './hooks/usePWA'; import { useThemeWatcher } from './hooks/useThemeWatcher'; import { useAppSelector } from './hooks/reduxHooks'; import { themeObject } from './styles/themes/themeVariables'; -import NDK, { NDKEvent, NDKNip07Signer, NDKRelayAuthPolicies } from '@nostr-dev-kit/ndk'; +import NDK, { NDKNip07Signer, NDKRelayAuthPolicies } from '@nostr-dev-kit/ndk'; import { useNDKInit } from '@nostr-dev-kit/ndk-hooks'; import config from './config/config'; diff --git a/src/components/relay-dashboard/paid-subscribers/PaidSubscribers.tsx b/src/components/relay-dashboard/paid-subscribers/PaidSubscribers.tsx index a41165c9..7fc56dde 100644 --- a/src/components/relay-dashboard/paid-subscribers/PaidSubscribers.tsx +++ b/src/components/relay-dashboard/paid-subscribers/PaidSubscribers.tsx @@ -20,6 +20,139 @@ import { UserOutlined } from '@ant-design/icons'; import { CreatorButton } from './avatar/SubscriberAvatar.styles'; const { Text } = Typography; +// LRU Cache implementation for profile caching +interface CachedProfile { + profile: SubscriberProfile; + timestamp: number; + accessCount: number; + lastAccessed: number; +} + +const PROFILE_CACHE_DURATION = 600000; // 10 minutes in milliseconds +const MAX_CACHE_SIZE = 5000; // Maximum number of cached profiles +const CLEANUP_INTERVAL = 300000; // Clean up every 5 minutes +const MAX_REQUEST_CACHE_SIZE = 100; // Maximum concurrent requests + +class ProfileCache { + private cache = new Map(); + private requestCache = new Map>(); + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor() { + this.startCleanupTimer(); + } + + private startCleanupTimer(): void { + this.cleanupTimer = setInterval(() => { + this.cleanup(); + }, CLEANUP_INTERVAL); + } + + private cleanup(): void { + const now = Date.now(); + const expiredKeys: string[] = []; + + // Find expired entries - convert to array first to avoid iterator issues + const cacheEntries = Array.from(this.cache.entries()); + for (const [key, cached] of cacheEntries) { + if (now - cached.timestamp > PROFILE_CACHE_DURATION) { + expiredKeys.push(key); + } + } + + // Remove expired entries + expiredKeys.forEach(key => this.cache.delete(key)); + + // If still over capacity, remove least recently used entries + if (this.cache.size > MAX_CACHE_SIZE) { + const entries = Array.from(this.cache.entries()); + entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed); + + const toRemove = entries.slice(0, this.cache.size - MAX_CACHE_SIZE); + toRemove.forEach(([key]) => this.cache.delete(key)); + } + + // Cleanup request cache if it gets too large + if (this.requestCache.size > MAX_REQUEST_CACHE_SIZE) { + this.requestCache.clear(); + } + + } + + getCachedProfile(pubkey: string): SubscriberProfile | null { + const cached = this.cache.get(pubkey); + if (!cached) return null; + + const isExpired = Date.now() - cached.timestamp > PROFILE_CACHE_DURATION; + if (isExpired) { + this.cache.delete(pubkey); + return null; + } + + // Update access statistics + cached.accessCount++; + cached.lastAccessed = Date.now(); + + return cached.profile; + } + + setCachedProfile(pubkey: string, profile: SubscriberProfile): void { + const now = Date.now(); + this.cache.set(pubkey, { + profile, + timestamp: now, + accessCount: 1, + lastAccessed: now + }); + + // Trigger cleanup if cache is getting too large + if (this.cache.size > MAX_CACHE_SIZE * 1.1) { + this.cleanup(); + } + } + + getRequestPromise(pubkey: string): Promise | null { + return this.requestCache.get(pubkey) || null; + } + + setRequestPromise(pubkey: string, promise: Promise): void { + this.requestCache.set(pubkey, promise); + + // Clean up when promise completes + promise.finally(() => { + this.requestCache.delete(pubkey); + }); + } + + getCacheStats(): { size: number; requestCacheSize: number } { + return { + size: this.cache.size, + requestCacheSize: this.requestCache.size + }; + } + + destroy(): void { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + this.cache.clear(); + this.requestCache.clear(); + } +} + +// Global profile cache instance +const globalProfileCache = new ProfileCache(); + +// Helper functions for backward compatibility +const getCachedProfile = (pubkey: string): SubscriberProfile | null => { + return globalProfileCache.getCachedProfile(pubkey); +}; + +const setCachedProfile = (pubkey: string, profile: SubscriberProfile): void => { + globalProfileCache.setCachedProfile(pubkey, profile); +}; + export const PaidSubscribers: React.FC = () => { const hookResult = usePaidSubscribers(12); const { subscribers, fetchMore, hasMore, loading, useDummyData } = hookResult; @@ -31,7 +164,6 @@ export const PaidSubscribers: React.FC = () => { // Modal state for view all subscribers const [isViewAllModalVisible, setIsViewAllModalVisible] = useState(false); - const [allSubscribers, setAllSubscribers] = useState([]); const [loadingProfiles, setLoadingProfiles] = useState(true); const [subscriberProfiles, setSubscriberProfiles] = useState>( @@ -68,11 +200,12 @@ export const PaidSubscribers: React.FC = () => { newMap.set(pubkey, profile); return newMap; }); + // Cache the profile globally + setCachedProfile(pubkey, profile); }; // Handle opening view all modal const handleViewAll = async () => { setIsViewAllModalVisible(true); - setAllSubscribers([...subscribers]); // Start with current subscribers // Fetch more subscribers if available let canFetchMore = hasMore; @@ -90,23 +223,77 @@ export const PaidSubscribers: React.FC = () => { }; useEffect(() => { - // Implement hybrid profile fetching: NDK first, fallback to backend data + // Implement hybrid profile fetching with 10-minute caching if (useDummyData) { setLoadingProfiles(false); return; } - const fetchProfiles = async () => { - if (!ndkInstance || !ndkInstance.ndk) { - setLoadingProfiles(false); - return; + const fetchSingleProfile = async (subscriber: SubscriberProfile): Promise => { + // Check if we already have a cached profile that's still valid + const cachedProfile = getCachedProfile(subscriber.pubkey); + if (cachedProfile) { + return cachedProfile; } + // Check if there's already a request in progress for this profile + const existingRequest = globalProfileCache.getRequestPromise(subscriber.pubkey); + if (existingRequest) { + return existingRequest; + } - // Process each subscriber with hybrid approach + // Create new request + const profileRequest = (async (): Promise => { + try { + + if (!ndkInstance || !ndkInstance.ndk) { + // No NDK available, return backend data + return { + ...subscriber, + name: subscriber.name || 'Anonymous Subscriber', + picture: subscriber.picture || '', + about: subscriber.about || '' + }; + } + + // Try to fetch profile from NDK (user's relay + other relays) + const user = await ndkInstance.ndk?.getUser({ pubkey: subscriber.pubkey }).fetchProfile(); + + if (user && (user.name || user.picture || user.about)) { + // NDK returned a profile - use it as the primary source + const ndkProfile = convertNDKUserProfileToSubscriberProfile(subscriber.pubkey, user); + return ndkProfile; + } else { + // NDK came up empty - fallback to backend data + return { + ...subscriber, + name: subscriber.name || 'Anonymous Subscriber', + picture: subscriber.picture || '', + about: subscriber.about || '' + }; + } + } catch (error) { + // Error occurred - fallback to backend data + return { + ...subscriber, + name: subscriber.name || 'Anonymous Subscriber', + picture: subscriber.picture || '', + about: subscriber.about || '' + }; + } + })(); + + // Store the promise in cache + globalProfileCache.setRequestPromise(subscriber.pubkey, profileRequest); + + return profileRequest; + }; + + const fetchProfiles = async () => { + // Process each subscriber with cached hybrid approach await Promise.all( subscribers.map(async (subscriber) => { - // Skip if we already have a complete profile in our map + // Skip if we already have a complete profile in our local map const existingProfile = subscriberProfiles.get(subscriber.pubkey); const hasValidProfile = existingProfile && ( (existingProfile.name && existingProfile.name !== 'Anonymous Subscriber') || @@ -119,30 +306,10 @@ export const PaidSubscribers: React.FC = () => { } try { - - // Try to fetch profile from NDK (user's relay + other relays) - const user = await ndkInstance.ndk?.getUser({ pubkey: subscriber.pubkey }).fetchProfile(); - - if (user && (user.name || user.picture || user.about)) { - // NDK returned a profile - use it as the primary source - - const ndkProfile = convertNDKUserProfileToSubscriberProfile(subscriber.pubkey, user); - updateSubscriberProfile(subscriber.pubkey, ndkProfile); - } else { - // NDK came up empty - fallback to backend data - - // Use the backend data as-is since NDK had no better information - updateSubscriberProfile(subscriber.pubkey, { - ...subscriber, - // Ensure we have fallback values if backend data is also incomplete - name: subscriber.name || 'Anonymous Subscriber', - picture: subscriber.picture || '', - about: subscriber.about || '' - }); - } + const profile = await fetchSingleProfile(subscriber); + updateSubscriberProfile(subscriber.pubkey, profile); } catch (error) { - - // Error occurred - fallback to backend data + // Use fallback profile updateSubscriberProfile(subscriber.pubkey, { ...subscriber, name: subscriber.name || 'Anonymous Subscriber', @@ -157,7 +324,7 @@ export const PaidSubscribers: React.FC = () => { }; fetchProfiles(); - }, [subscribers, ndkInstance]); + }, [subscribers, ndkInstance, useDummyData, subscriberProfiles]); // Handle closing view all modal const handleCloseViewAllModal = () => { diff --git a/src/components/relay-settings/layouts/DesktopLayout.tsx b/src/components/relay-settings/layouts/DesktopLayout.tsx index 84579739..52ffc833 100644 --- a/src/components/relay-settings/layouts/DesktopLayout.tsx +++ b/src/components/relay-settings/layouts/DesktopLayout.tsx @@ -9,7 +9,6 @@ import { TotalEarning } from '@app/components/relay-dashboard/totalEarning/Total import { ActivityStory } from '@app/components/relay-dashboard/transactions/Transactions'; import * as S from '@app/pages/uiComponentsPages/UIComponentsPage.styles'; import { NetworkSection } from '@app/components/relay-settings/sections/NetworkSection'; -import { AppBucketsSection } from '@app/components/relay-settings/sections/AppBucketsSection'; 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'; @@ -25,13 +24,6 @@ interface DesktopLayoutProps { isFileStorageActive: boolean; onProtocolsChange: (protocols: string[]) => void; onFileStorageChange: (active: boolean) => void; - // App buckets section props - appBuckets: string[]; - dynamicAppBuckets: string[]; - onAppBucketsChange: (values: string[]) => void; - onDynamicAppBucketsChange: (values: string[]) => void; - onAddBucket: (bucket: string) => void; - onRemoveBucket: (bucket: string) => void; // Kinds section props isKindsActive: boolean; selectedKinds: string[]; @@ -82,13 +74,6 @@ export const DesktopLayout: React.FC = ({ isFileStorageActive, onProtocolsChange, onFileStorageChange, - // App buckets props - appBuckets, - dynamicAppBuckets, - onAppBucketsChange, - onDynamicAppBucketsChange, - onAddBucket, - onRemoveBucket, // Kinds props isKindsActive, selectedKinds, @@ -125,14 +110,6 @@ export const DesktopLayout: React.FC = ({ onFileStorageChange={onFileStorageChange} /> - void; onFileStorageChange: (active: boolean) => void; - // App buckets section props - appBuckets: string[]; - dynamicAppBuckets: string[]; - onAppBucketsChange: (values: string[]) => void; - onDynamicAppBucketsChange: (values: string[]) => void; - onAddBucket: (bucket: string) => void; - onRemoveBucket: (bucket: string) => void; // Kinds section props isKindsActive: boolean; selectedKinds: string[]; @@ -79,13 +71,6 @@ export const MobileLayout: React.FC = ({ isFileStorageActive, onProtocolsChange, onFileStorageChange, - // App buckets props - appBuckets, - dynamicAppBuckets, - onAppBucketsChange, - onDynamicAppBucketsChange, - onAddBucket, - onRemoveBucket, // Kinds props isKindsActive, selectedKinds, @@ -120,15 +105,6 @@ export const MobileLayout: React.FC = ({ onFileStorageChange={onFileStorageChange} /> - - void; - onDynamicAppBucketsChange: (values: string[]) => void; - onAddBucket: (bucket: string) => void; - onRemoveBucket: (bucket: string) => void; -} - -export const AppBucketsSection: React.FC = ({ - appBuckets, - dynamicAppBuckets, - onAppBucketsChange, - onDynamicAppBucketsChange, - onAddBucket, - onRemoveBucket, -}) => { - const theme = useAppSelector((state) => state.theme.theme); - - return ( - - -
- - - - - - {'Enabling buckets will organize data stored within the relay to quicken retrieval times for users. Disabling buckets will not turn off data storage.'} - - - - - - - -
-
-
- ); -}; - -export default AppBucketsSection; \ No newline at end of file diff --git a/src/components/relay-settings/sections/AppBucketsSection/components/AddBucketForm.tsx b/src/components/relay-settings/sections/AppBucketsSection/components/AddBucketForm.tsx deleted file mode 100644 index 49159065..00000000 --- a/src/components/relay-settings/sections/AppBucketsSection/components/AddBucketForm.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// src/components/relay-settings/sections/AppBucketsSection/components/AddBucketForm.tsx - -import React, { useState } from 'react'; -import { Input } from 'antd'; -import { BaseButton } from '@app/components/common/BaseButton/BaseButton'; -import * as S from '@app/pages/uiComponentsPages/UIComponentsPage.styles'; - -interface AddBucketFormProps { - onAddBucket: (bucket: string) => void; -} - -export const AddBucketForm: React.FC = ({ onAddBucket }) => { - const [newBucket, setNewBucket] = useState(''); - - const handleAddBucket = () => { - if (newBucket) { - onAddBucket(newBucket); - setNewBucket(''); - } - }; - - return ( -
-

{'Add an App Bucket'}

-
- setNewBucket(e.target.value)} - placeholder="Enter new app bucket" - /> - - Add bucket - -
-
- ); -}; - -export default AddBucketForm; \ No newline at end of file diff --git a/src/components/relay-settings/sections/AppBucketsSection/components/BucketsList.tsx b/src/components/relay-settings/sections/AppBucketsSection/components/BucketsList.tsx deleted file mode 100644 index 60ab2c2c..00000000 --- a/src/components/relay-settings/sections/AppBucketsSection/components/BucketsList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// src/components/relay-settings/sections/AppBucketsSection/components/BucketsList.tsx - -import React from 'react'; -import { CheckboxValueType } from 'antd/es/checkbox/Group'; -import { BaseCheckbox } from '@app/components/common/BaseCheckbox/BaseCheckbox'; -import * as S from '@app/pages/uiComponentsPages/UIComponentsPage.styles'; -import { appBuckets as defaultAppBuckets } from '@app/constants/relaySettings'; -import { themeObject } from '@app/styles/themes/themeVariables'; -import { useAppSelector } from '@app/hooks/reduxHooks'; - -interface BucketsListProps { - selectedBuckets: string[]; - onBucketsChange: (values: string[]) => void; -} - -export const BucketsList: React.FC = ({ - selectedBuckets, - onBucketsChange, -}) => { - const theme = useAppSelector((state) => state.theme.theme); - - const bucketOptions = defaultAppBuckets.map(bucket => ({ - label: ( - - {bucket.label} - - ), - value: bucket.id, - })); - - const handleChange = (checkedValues: CheckboxValueType[]) => { - onBucketsChange(checkedValues as string[]); - }; - - return ( - - ); -}; - -export default BucketsList; \ No newline at end of file diff --git a/src/components/relay-settings/sections/AppBucketsSection/components/DynamicBucketsList.tsx b/src/components/relay-settings/sections/AppBucketsSection/components/DynamicBucketsList.tsx deleted file mode 100644 index 1a45a3f7..00000000 --- a/src/components/relay-settings/sections/AppBucketsSection/components/DynamicBucketsList.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// src/components/relay-settings/sections/AppBucketsSection/components/DynamicBucketsList.tsx - -import React from 'react'; -import { CheckboxValueType } from 'antd/es/checkbox/Group'; -import { BaseCheckbox } from '@app/components/common/BaseCheckbox/BaseCheckbox'; -import { BaseButton } from '@app/components/common/BaseButton/BaseButton'; -import * as S from '@app/pages/uiComponentsPages/UIComponentsPage.styles'; - -interface DynamicBucketsListProps { - buckets: string[]; - selectedBuckets: string[]; - onBucketsChange: (values: string[]) => void; - onRemoveBucket: (bucket: string) => void; -} - -export const DynamicBucketsList: React.FC = ({ - buckets, - selectedBuckets, - onBucketsChange, - onRemoveBucket, -}) => { - const handleChange = (checkedValues: CheckboxValueType[]) => { - onBucketsChange(checkedValues as string[]); - }; - - return ( - - {buckets.map((bucket) => ( -
-
- - - {bucket} - -
- onRemoveBucket(bucket)} - > - Remove - -
- ))} -
- ); -}; - -export default DynamicBucketsList; \ No newline at end of file diff --git a/src/components/relay-settings/sections/AppBucketsSection/index.ts b/src/components/relay-settings/sections/AppBucketsSection/index.ts deleted file mode 100644 index 8f3d9f92..00000000 --- a/src/components/relay-settings/sections/AppBucketsSection/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// src/components/relay-settings/sections/AppBucketsSection/index.ts - -import AppBucketsSection from './AppBucketsSection'; -export { AppBucketsSection }; diff --git a/src/constants/coreKinds.ts b/src/constants/coreKinds.ts index c58df4d7..fb6bcac9 100644 --- a/src/constants/coreKinds.ts +++ b/src/constants/coreKinds.ts @@ -27,7 +27,7 @@ export const isCoreKind = (kind: string): boolean => { // Helper function to get all possible kinds from noteOptions export const getAllPossibleKinds = (): string[] => { // Import noteOptions dynamically to avoid circular dependency - const { noteOptions } = require('../constants/relaySettings'); + const { noteOptions } = require('./relaySettings'); return noteOptions.map((option: any) => option.kindString); }; @@ -40,4 +40,34 @@ export const calculateInverseKinds = (selectedKinds: string[]): string[] => { !selectedKinds.includes(kind) || isCoreKind(kind) ); return allowedKinds; +}; + +// Media type helper functions +export const getAllPossibleMediaTypes = (mediaType: 'photos' | 'videos' | 'audio'): string[] => { + // Import mimeTypeOptions dynamically to avoid circular dependency + const { mimeTypeOptions } = require('./relaySettings'); + + switch (mediaType) { + case 'photos': + return mimeTypeOptions + .filter((option: any) => option.value.startsWith('image/') || option.value === 'application/pdf' || option.value === 'application/postscript') + .map((option: any) => option.value); + case 'videos': + return mimeTypeOptions + .filter((option: any) => option.value.startsWith('video/')) + .map((option: any) => option.value); + case 'audio': + return mimeTypeOptions + .filter((option: any) => option.value.startsWith('audio/')) + .map((option: any) => option.value); + default: + return []; + } +}; + +// Helper function to calculate inverse for media types in blacklist mode +export const calculateInverseMediaTypes = (selectedMediaTypes: string[], mediaType: 'photos' | 'videos' | 'audio'): string[] => { + const allPossibleMediaTypes = getAllPossibleMediaTypes(mediaType); + // In blacklist mode: selected = blocked, so return all types except the selected (blocked) ones + return allPossibleMediaTypes.filter(type => !selectedMediaTypes.includes(type)); }; \ No newline at end of file diff --git a/src/constants/relaySettings.ts b/src/constants/relaySettings.ts index 09dd09f3..19a84c40 100644 --- a/src/constants/relaySettings.ts +++ b/src/constants/relaySettings.ts @@ -7,8 +7,6 @@ export type Settings = { videos: string[]; gitNestr: string[]; audio: string[]; - appBuckets: string[]; - dynamicAppBuckets: string[]; isKindsActive: boolean; isPhotosActive: boolean; isVideosActive: boolean; @@ -22,7 +20,7 @@ export type Settings = { audioMaxSizeMB: number; } -export type Category = 'kinds' | 'photos' | 'videos' | 'gitNestr' | 'audio' | 'dynamicKinds' | 'appBuckets' | 'dynamicAppBuckets' | 'photoMaxSizeMB' | 'videoMaxSizeMB' | 'audioMaxSizeMB'; +export type Category = 'kinds' | 'photos' | 'videos' | 'gitNestr' | 'audio' | 'dynamicKinds' | 'photoMaxSizeMB' | 'videoMaxSizeMB' | 'audioMaxSizeMB'; export const noteOptions = [ { kind: 0, kindString: 'kind0', description: 'Metadata', category: 1 }, { kind: 1, kindString: 'kind1', description: 'Text Note', category: 1 }, @@ -54,17 +52,6 @@ export const noteOptions = [ { kind: 19842, kindString: 'kind19842', description: 'Storage Metadata', category: 1 }, { kind: 19843, kindString: 'kind19843', description: 'Storage Delete', category: 1 }, ]; -export const appBuckets = [ - { id: 'nostr', label: 'Nostr' }, - { - id: 'gitnestr', - label: 'GitNestr', - }, - { - id: 'NostrBox', - label: 'NostrBox', - }, -]; export const categories = [ { id: 1, name: 'Basic Nostr Features' }, { id: 2, name: 'Extra Nostr Features' }, diff --git a/src/hooks/usePaidSubscribers.ts b/src/hooks/usePaidSubscribers.ts index ef00963e..140480db 100644 --- a/src/hooks/usePaidSubscribers.ts +++ b/src/hooks/usePaidSubscribers.ts @@ -58,6 +58,16 @@ const dummyProfiles: SubscriberProfile[] = [ // URL of the placeholder avatar that comes from the API const PLACEHOLDER_AVATAR_URL = 'http://localhost:3000/placeholder-avatar.png'; +// Global cache for subscriber data with 10-minute TTL +interface SubscriberCache { + data: SubscriberProfile[]; + timestamp: number; + hasMore: boolean; +} + +const SUBSCRIBER_CACHE_DURATION = 600000; // 10 minutes in milliseconds +const globalSubscriberCache = new Map(); + const usePaidSubscribers = (pageSize = 20) => { const [subscribers, setSubscribers] = useState([]); const [loading, setLoading] = useState(false); @@ -71,11 +81,9 @@ const usePaidSubscribers = (pageSize = 20) => { const fetchSubscribers = useCallback(async (reset = false) => { try { - console.log('[usePaidSubscribers] Starting to fetch subscribers...'); setLoading(true); const token = readToken(); if (!token) { - console.log('[usePaidSubscribers] No authentication token found, using dummy data'); setUseDummyData(true); setSubscribers(dummyProfiles); setHasMore(false); @@ -83,14 +91,24 @@ const usePaidSubscribers = (pageSize = 20) => { } const page = reset ? 1 : currentPage; + const cacheKey = `${page}-${pageSize}`; + + // Check cache first + const cached = globalSubscriberCache.get(cacheKey); + if (cached && (Date.now() - cached.timestamp) < SUBSCRIBER_CACHE_DURATION) { + setSubscribers(cached.data); + setHasMore(cached.hasMore); + setUseDummyData(false); + setCurrentPage(page + 1); + return; + } + const queryParams = new URLSearchParams({ page: page.toString(), limit: pageSize.toString(), }); const requestUrl = `${config.baseURL}/api/paid-subscriber-profiles?${queryParams}`; - console.log(`[usePaidSubscribers] Fetching from URL: ${requestUrl}`); - console.log(`[usePaidSubscribers] Current baseURL: ${config.baseURL}`); const response = await fetch(requestUrl, { headers: { @@ -99,31 +117,22 @@ const usePaidSubscribers = (pageSize = 20) => { }, }); - console.log(`[usePaidSubscribers] Response status: ${response.status}`); if (!response.ok) { if (response.status === 401) { handleLogout(); - console.log('[usePaidSubscribers] Authentication failed, using dummy data'); - return; + return; } throw new Error(`Request failed: ${response.status}`); } - // Clone the response before consuming it with json() so we can log the raw text if needed - const responseClone = response.clone(); let data: SubscriberProfile[] = []; try { data = await response.json(); - console.log('[usePaidSubscribers] Response data (raw):', data); - console.log('[usePaidSubscribers] Data constructor:', data.constructor?.name); - console.log('[usePaidSubscribers] Data properties:', Object.getOwnPropertyNames(data)); - console.log('[usePaidSubscribers] JSON.stringify(data):', JSON.stringify(data)); // Ensure data is always an array if (!Array.isArray(data)) { - console.warn('[usePaidSubscribers] Data is not an array, forcing to array format'); if (data && typeof data === 'object') { // If data is an object but not an array, try to convert it if (Object.keys(data).length > 0) { @@ -137,25 +146,17 @@ const usePaidSubscribers = (pageSize = 20) => { } } catch (jsonError) { console.error('[usePaidSubscribers] Error parsing JSON response:', jsonError); - // Try to get the raw text to see what's being returned - const rawText = await responseClone.text(); - console.log('[usePaidSubscribers] Raw response text:', rawText); data = []; } - console.log(`[usePaidSubscribers] Normalized data:`, data); - console.log(`[usePaidSubscribers] Data length: ${data?.length}, typeof data: ${typeof data}, Array.isArray(data): ${Array.isArray(data)}`); // If we have backend data, use it as the primary source and return subscribers for NDK enhancement if (data && Array.isArray(data) && data.length > 0) { - console.log(`[usePaidSubscribers] Backend data detected, using as primary source`); try { // Process the profiles to replace placeholder avatar URLs const processedProfiles: SubscriberProfile[] = []; - console.log(`[usePaidSubscribers] First item pubkey:`, data[0]?.pubkey); - console.log(`[usePaidSubscribers] First item picture:`, data[0]?.picture); for (const profile of data) { if (!profile || !profile.pubkey) { @@ -168,7 +169,6 @@ const usePaidSubscribers = (pageSize = 20) => { let pictureUrl = profile.picture; if (usesPlaceholder) { - console.log(`[usePaidSubscribers] Replacing placeholder for ${profile.pubkey}`); pictureUrl = adminDefaultAvatar; } @@ -181,8 +181,6 @@ const usePaidSubscribers = (pageSize = 20) => { }); } - console.log('[usePaidSubscribers] Backend data processed successfully'); - console.log('[usePaidSubscribers] Processed profiles count:', processedProfiles.length); // Update state with backend data setUseDummyData(false); @@ -190,7 +188,13 @@ const usePaidSubscribers = (pageSize = 20) => { setHasMore(data.length === pageSize); setCurrentPage(page + 1); - console.log('[usePaidSubscribers] Backend data set as primary source'); + // Cache the successful result + globalSubscriberCache.set(cacheKey, { + data: processedProfiles, + timestamp: Date.now(), + hasMore: data.length === pageSize + }); + return; // Exit early after processing backend data } catch (processingError) { console.error('[usePaidSubscribers] Error processing backend profiles:', processingError); @@ -200,14 +204,11 @@ const usePaidSubscribers = (pageSize = 20) => { // Fallback logic if no backend data - only use dummy data when truly no data available if (isMounted.current) { - console.log('[usePaidSubscribers] No backend data found'); // Only use dummy data if we have absolutely nothing and no existing real subscribers if (subscribers.length === 0 && !subscribers.some(s => !s.pubkey.startsWith('dummy-'))) { - console.log('[usePaidSubscribers] No existing subscribers, using dummy data as fallback'); setUseDummyData(true); setSubscribers(dummyProfiles); } else { - console.log('[usePaidSubscribers] Keeping existing subscribers data or have real subscribers'); setUseDummyData(false); } setHasMore(false); @@ -219,11 +220,9 @@ const usePaidSubscribers = (pageSize = 20) => { // Only use dummy data if we don't have any real subscribers if (subscribers.length === 0 || subscribers.every(s => s.pubkey.startsWith('dummy-'))) { - console.log(`[usePaidSubscribers] ${errorMessage}, using dummy data`); setUseDummyData(true); setSubscribers(dummyProfiles); } else { - console.log(`[usePaidSubscribers] ${errorMessage}, keeping existing real subscribers`); setUseDummyData(false); } setHasMore(false); @@ -231,20 +230,16 @@ const usePaidSubscribers = (pageSize = 20) => { if (isMounted.current) { setLoading(false); } - console.log('[usePaidSubscribers] Fetch operation completed'); } - }, [currentPage, pageSize, handleLogout]); + }, [currentPage, pageSize, handleLogout, subscribers]); useEffect(() => { - console.log('[usePaidSubscribers] Hook mounted'); return () => { - console.log('[usePaidSubscribers] Hook unmounting'); isMounted.current = false; }; }, []); useEffect(() => { - console.log('[usePaidSubscribers] Initial fetch triggered'); fetchSubscribers(true); }, [fetchSubscribers]); diff --git a/src/hooks/useRelaySettings.ts b/src/hooks/useRelaySettings.ts index 2e9c61e3..496ed4c2 100644 --- a/src/hooks/useRelaySettings.ts +++ b/src/hooks/useRelaySettings.ts @@ -3,7 +3,7 @@ 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 } from '@app/constants/coreKinds'; +import { CORE_KINDS, ensureCoreKinds, calculateInverseKinds, getAllPossibleKinds, isCoreKind, getAllPossibleMediaTypes, calculateInverseMediaTypes } from '@app/constants/coreKinds'; // Legacy interface - no longer used with new API // interface BackendRelaySettings { ... } @@ -17,8 +17,6 @@ const getInitialSettings = (): Settings => ({ videos: [], gitNestr: [], audio: [], - appBuckets: [], - dynamicAppBuckets: [], isKindsActive: true, isPhotosActive: true, isVideosActive: true, @@ -45,40 +43,42 @@ const useRelaySettings = () => { const token = readToken(); // Keep track of the last mode to prevent unnecessary updates - const lastMode = useRef(relaySettings.mode); + const lastMode = useRef(null); /* eslint-disable react-hooks/exhaustive-deps */ - // Effect to handle mode changes + // 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; } - // Only clear arrays when user manually switches TO blacklist mode - // Don't clear if we're in blacklist mode and already have blocked items (loaded from backend) + 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') { - // Only clear if we don't have any blocked items (user switching, not loading from backend) - const hasBlockedItems = relaySettings.kinds.length > 0 || relaySettings.photos.length > 0 || - relaySettings.videos.length > 0 || relaySettings.audio.length > 0; - - if (!hasBlockedItems) { - // Store current settings before clearing - setPreviousSmartSettings({ - kinds: relaySettings.kinds, - photos: relaySettings.photos, - videos: relaySettings.videos, - audio: relaySettings.audio, - }); + // Store current whitelist settings before clearing + setPreviousSmartSettings({ + kinds: relaySettings.kinds, + photos: relaySettings.photos, + videos: relaySettings.videos, + audio: relaySettings.audio, + }); - // Clear selections in blacklist mode - user starts fresh and selects what to block - setRelaySettings(prev => ({ - ...prev, - kinds: [], - photos: [], - videos: [], - 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 => ({ @@ -103,6 +103,12 @@ const useRelaySettings = () => { 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'; @@ -113,9 +119,16 @@ const useRelaySettings = () => { if (settings.mode === 'blacklist') { // In blacklist mode: backend sends allowed kinds, we need to calculate blocked kinds const allPossibleKinds = getAllPossibleKinds(); - const blockedKinds = allPossibleKinds.filter(kind => !backendKinds.includes(kind)); + const backendKindsSet = new Set(backendKinds); + const allKindsSet = new Set(allPossibleKinds); + + // Calculate blocked kinds: all possible kinds minus backend allowed kinds + const blockedKinds = Array.from(allKindsSet).filter(kind => !backendKindsSet.has(kind)); // Only show non-core kinds as blocked (core kinds can't be blocked) settings.kinds = blockedKinds.filter(kind => !isCoreKind(kind)); + + console.log('[useRelaySettings] Blacklist mode - Backend allowed kinds:', backendKinds); + console.log('[useRelaySettings] Blacklist mode - Calculated blocked kinds:', settings.kinds); } else { // In whitelist mode: backend sends allowed kinds directly settings.kinds = ensureCoreKinds(backendKinds); @@ -124,9 +137,38 @@ const useRelaySettings = () => { // 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 - settings.photos = mediaDefinitions.image?.mime_patterns || mediaDefinitions.image?.mimepatterns || []; - settings.videos = mediaDefinitions.video?.mime_patterns || mediaDefinitions.video?.mimepatterns || []; - settings.audio = mediaDefinitions.audio?.mime_patterns || mediaDefinitions.audio?.mimepatterns || []; + 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 || []; + + // 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; + } // Extract file size limits (handle both old and new field names) settings.photoMaxSizeMB = mediaDefinitions.image?.max_size_mb || mediaDefinitions.image?.maxsizemb || 100; @@ -141,7 +183,8 @@ const useRelaySettings = () => { } } - // Store these as the previous whitelist settings if in whitelist mode + // 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, @@ -162,20 +205,33 @@ 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 const mediaDefinitions = { image: { - mime_patterns: settings.photos, // Only send correct field name + mime_patterns: photoMimePatterns, // Send processed mime patterns based on mode extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"], max_size_mb: settings.photoMaxSizeMB // Only send correct field name }, video: { - mime_patterns: settings.videos, // Only send correct field name + mime_patterns: videoMimePatterns, // Send processed mime patterns based on mode extensions: [".mp4", ".webm", ".avi", ".mov"], max_size_mb: settings.videoMaxSizeMB // Only send correct field name }, audio: { - mime_patterns: settings.audio, // Only send correct field name + mime_patterns: audioMimePatterns, // Send processed mime patterns based on mode extensions: [".mp3", ".wav", ".ogg", ".flac"], max_size_mb: settings.audioMaxSizeMB // Only send correct field name } diff --git a/src/pages/RelaySettingsPage.tsx b/src/pages/RelaySettingsPage.tsx index 88c1f487..8f6b9454 100644 --- a/src/pages/RelaySettingsPage.tsx +++ b/src/pages/RelaySettingsPage.tsx @@ -10,7 +10,7 @@ import { useResponsive } from '@app/hooks/useResponsive'; import useRelaySettings from '@app/hooks/useRelaySettings'; import { DesktopLayout } from '@app/components/relay-settings/layouts/DesktopLayout'; import { MobileLayout } from '@app/components/relay-settings/layouts/MobileLayout'; -import { Settings, Category } from '@app/constants/relaySettings'; +import { Settings } from '@app/constants/relaySettings'; const RelaySettingsPage: React.FC = () => { const { t } = useTranslation(); @@ -33,8 +33,6 @@ const RelaySettingsPage: React.FC = () => { videos: [], gitNestr: [], audio: [], - appBuckets: [], - dynamicAppBuckets: [], isKindsActive: true, isPhotosActive: true, isVideosActive: true, @@ -53,19 +51,6 @@ const RelaySettingsPage: React.FC = () => { JSON.parse(localStorage.getItem('dynamicKinds') || '[]'), ); - const [dynamicAppBuckets, setDynamicAppBuckets] = useState( - JSON.parse(localStorage.getItem('dynamicAppBuckets') || '[]'), - ); - - // Blacklist state - const [blacklist, setBlacklist] = useState({ - kinds: [], - photos: [], - videos: [], - gitNestr: [], - audio: [], - }); - // Fetch initial settings useEffect(() => { fetchSettings(); @@ -74,28 +59,13 @@ const RelaySettingsPage: React.FC = () => { // Sync settings with relay settings useEffect(() => { if (relaySettings) { - console.log('Raw relay settings:', relaySettings); // For debugging - - setSettings(prev => ({ + setSettings({ ...relaySettings, protocol: Array.isArray(relaySettings.protocol) ? relaySettings.protocol : [relaySettings.protocol] - })); - setDynamicAppBuckets(relaySettings.dynamicAppBuckets); + }); } }, [relaySettings]); - // Reset blacklist when mode changes - useEffect(() => { - if (settings.mode === 'blacklist') return; - setBlacklist({ - kinds: [], - photos: [], - videos: [], - gitNestr: [], - audio: [], - }); - }, [settings.mode]); - const handleModeChange = (checked: boolean) => { const newMode = checked ? 'whitelist' : 'blacklist'; setSettings(prev => ({ @@ -127,8 +97,6 @@ const RelaySettingsPage: React.FC = () => { updateSettings('audio', settings.isAudioActive ? settings.audio : []), updateSettings('protocol', settings.protocol), updateSettings('isFileStorageActive', settings.isFileStorageActive), - updateSettings('appBuckets', settings.appBuckets), - updateSettings('dynamicAppBuckets', settings.dynamicAppBuckets), updateSettings('moderationMode', settings.moderationMode), ]); @@ -153,35 +121,6 @@ const RelaySettingsPage: React.FC = () => { updateSettings('isFileStorageActive', active); }; - // App buckets handlers - const handleAppBucketsChange = (values: string[]) => { - setSettings(prev => ({ ...prev, appBuckets: values })); - updateSettings('appBuckets', values); - }; - - const handleDynamicAppBucketsChange = (values: string[]) => { - setSettings(prev => ({ ...prev, dynamicAppBuckets: values })); - updateSettings('dynamicAppBuckets', values); - }; - - const handleAddBucket = (bucket: string) => { - if (!bucket || dynamicAppBuckets.includes(bucket)) return; - - const updatedBuckets = [...dynamicAppBuckets, bucket]; - setDynamicAppBuckets(updatedBuckets); - setSettings(prev => ({ ...prev, dynamicAppBuckets: updatedBuckets })); - updateSettings('dynamicAppBuckets', updatedBuckets); - localStorage.setItem('dynamicAppBuckets', JSON.stringify(updatedBuckets)); - }; - - const handleRemoveBucket = (bucket: string) => { - const updatedBuckets = dynamicAppBuckets.filter(b => b !== bucket); - setDynamicAppBuckets(updatedBuckets); - setSettings(prev => ({ ...prev, dynamicAppBuckets: updatedBuckets })); - updateSettings('dynamicAppBuckets', updatedBuckets); - localStorage.setItem('dynamicAppBuckets', JSON.stringify(updatedBuckets)); - }; - // Kinds section handlers const handleKindsActiveChange = (active: boolean) => { setSettings(prev => ({ ...prev, isKindsActive: active })); @@ -246,13 +185,6 @@ const RelaySettingsPage: React.FC = () => { isFileStorageActive: settings.isFileStorageActive, onProtocolsChange: handleProtocolChange, onFileStorageChange: handleFileStorageChange, - // App buckets props - appBuckets: settings.appBuckets, - dynamicAppBuckets: settings.dynamicAppBuckets, - onAppBucketsChange: handleAppBucketsChange, - onDynamicAppBucketsChange: handleDynamicAppBucketsChange, - onAddBucket: handleAddBucket, - onRemoveBucket: handleRemoveBucket, // Kinds props isKindsActive: settings.isKindsActive, selectedKinds: settings.kinds, From f4d1ead1161944828b8a42b026e5d314376bd39d Mon Sep 17 00:00:00 2001 From: Maphikza Date: Fri, 4 Jul 2025 22:40:02 +0200 Subject: [PATCH 3/3] Fix dynamic kinds handling in blacklist mode - Properly include dynamic kinds in backend whitelist calculation - Separate predefined vs dynamic kinds in reconciliation logic - Fix kind formatting in AddKindForm - Improve UI labels for better UX - Show dynamic kinds in both whitelist and blacklist modes --- .../KindsSection/components/AddKindForm.tsx | 12 +++--- .../components/DynamicKindsList.tsx | 4 +- src/hooks/useRelaySettings.ts | 40 +++++++++++++++---- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/components/relay-settings/sections/KindsSection/components/AddKindForm.tsx b/src/components/relay-settings/sections/KindsSection/components/AddKindForm.tsx index a2b70c66..4f1805e1 100644 --- a/src/components/relay-settings/sections/KindsSection/components/AddKindForm.tsx +++ b/src/components/relay-settings/sections/KindsSection/components/AddKindForm.tsx @@ -14,23 +14,21 @@ export const AddKindForm: React.FC = ({ onAddKind, mode }) => const handleAddKind = () => { if (newKind) { - onAddKind(newKind); + // Ensure the kind is in the correct format + const formattedKind = newKind.startsWith('kind') ? newKind : `kind${newKind}`; + onAddKind(formattedKind); setNewKind(''); } }; - if (mode === 'whitelist') { - return null; - } - return (
-

{'Add to Blacklist'}

+

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

setNewKind(e.target.value)} - placeholder="Enter new kind" + placeholder="Enter kind number (e.g., 12345)" /> Add Kind diff --git a/src/components/relay-settings/sections/KindsSection/components/DynamicKindsList.tsx b/src/components/relay-settings/sections/KindsSection/components/DynamicKindsList.tsx index 07c52a46..8324b961 100644 --- a/src/components/relay-settings/sections/KindsSection/components/DynamicKindsList.tsx +++ b/src/components/relay-settings/sections/KindsSection/components/DynamicKindsList.tsx @@ -21,7 +21,7 @@ export const DynamicKindsList: React.FC = ({ onRemoveKind, mode, }) => { - if (!dynamicKinds.length || mode === 'whitelist') { + if (!dynamicKinds.length) { return null; } @@ -50,7 +50,7 @@ export const DynamicKindsList: React.FC = ({ isActive={true} style={{ fontSize: '1rem', paddingRight: '.8rem', paddingLeft: '.8rem' }} > - {`kind` + kind} + {kind}
{ // Handle kinds based on mode 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 kinds: all possible kinds minus backend allowed kinds - const blockedKinds = Array.from(allKindsSet).filter(kind => !backendKindsSet.has(kind)); + // 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 = blockedKinds.filter(kind => !isCoreKind(kind)); + 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 kinds:', settings.kinds); + 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 - settings.kinds = ensureCoreKinds(backendKinds); + // 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; } // Extract mime types and file sizes from actual backend format @@ -242,9 +255,20 @@ const useRelaySettings = () => { event_filtering: { mode: settings.mode, moderation_mode: settings.moderationMode, - kind_whitelist: settings.mode === 'blacklist' - ? calculateInverseKinds(settings.kinds) // For blacklist: send inverse (all kinds except blocked ones) - : ensureCoreKinds(settings.kinds), // For whitelist: send selected kinds directly + 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]); + } + })(), media_definitions: mediaDefinitions, dynamic_kinds: { enabled: false,