From 3ad83cd1143960f22db42f19ec735a013d058548 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 4 Jun 2025 10:04:39 -0500 Subject: [PATCH] Refactor AppProvider --- src/App.tsx | 8 +- src/components/AppProvider.ts | 189 ------------------------------- src/components/AppProvider.tsx | 82 ++++++++++++++ src/components/NostrProvider.tsx | 10 +- src/components/RelaySelector.tsx | 8 +- src/contexts/AppContext.ts | 19 ++++ src/hooks/useAppConfig.ts | 14 +++ src/hooks/useTheme.ts | 22 +++- src/test/TestApp.tsx | 8 +- 9 files changed, 156 insertions(+), 204 deletions(-) delete mode 100644 src/components/AppProvider.ts create mode 100644 src/components/AppProvider.tsx create mode 100644 src/contexts/AppContext.ts create mode 100644 src/hooks/useAppConfig.ts diff --git a/src/App.tsx b/src/App.tsx index f8d0a19..69b878b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { NostrLoginProvider } from '@nostrify/react/login'; import { AppProvider } from '@/components/AppProvider'; +import { AppConfig } from '@/contexts/AppContext'; import AppRouter from './AppRouter'; const queryClient = new QueryClient({ @@ -21,9 +22,14 @@ const queryClient = new QueryClient({ }, }); +const defaultConfig: AppConfig = { + theme: "light", + relayUrl: "wss://relay.nostr.band", +}; + export function App() { return ( - + diff --git a/src/components/AppProvider.ts b/src/components/AppProvider.ts deleted file mode 100644 index a765c88..0000000 --- a/src/components/AppProvider.ts +++ /dev/null @@ -1,189 +0,0 @@ -import React, { createContext, useContext, ReactNode, useEffect } from 'react'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; - -export type Theme = "dark" | "light" | "system"; - -interface AppConfig { - /** Current theme */ - theme: Theme; - /** Selected relay URL */ - relayUrl: string; -} - -interface RelayInfo { - /** Relay URL */ - url: string; - /** Display name for the relay */ - name: string; -} - -// Available relay options -export const RELAY_OPTIONS: RelayInfo[] = [ - { url: 'wss://ditto.pub/relay', name: 'Ditto' }, - { url: 'wss://relay.nostr.band', name: 'Nostr.Band' }, - { url: 'wss://relay.damus.io', name: 'Damus' }, - { url: 'wss://relay.primal.net', name: 'Primal' }, -]; - -// Default application configuration -const DEFAULT_CONFIG: AppConfig = { - theme: 'system', - relayUrl: 'wss://relay.nostr.band', -}; - -interface AppContextType { - /** Current application configuration */ - config: AppConfig; - /** Update configuration using a callback that receives current config and returns new config */ - updateConfig: (updater: (currentConfig: AppConfig) => AppConfig) => void; - /** Available relay options */ - availableRelays: RelayInfo[]; -} - -const AppContext = createContext(undefined); - -const APP_CONFIG_STORAGE_KEY = 'nostr:app-config'; - -interface AppProviderProps { - children: ReactNode; - /** Default theme for the application */ - defaultTheme?: Theme; -} - -export function AppProvider({ - children, - defaultTheme = 'system' -}: AppProviderProps) { - // App configuration state with localStorage persistence - const [config, setConfig] = useLocalStorage( - APP_CONFIG_STORAGE_KEY, - { ...DEFAULT_CONFIG, theme: defaultTheme } - ); -/** - * Hook to apply theme changes to the document root - */ -function useThemeEffect(theme: Theme) { - useEffect(() => { - const root = window.document.documentElement; - - root.classList.remove('light', 'dark'); - - if (theme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') - .matches - ? 'dark' - : 'light'; - - root.classList.add(systemTheme); - return; - } - - root.classList.add(theme); - }, [theme]); - - // Handle system theme changes when theme is set to "system" - useEffect(() => { - if (theme !== 'system') return; - - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - - const handleChange = () => { - const root = window.document.documentElement; - root.classList.remove('light', 'dark'); - - const systemTheme = mediaQuery.matches ? 'dark' : 'light'; - root.classList.add(systemTheme); - }; - - mediaQuery.addEventListener('change', handleChange); - return () => mediaQuery.removeEventListener('change', handleChange); - }, [theme]); -} - // Apply theme effects to document - useThemeEffect(config.theme); - - // Generic config updater with callback pattern - const updateConfig = (updater: (currentConfig: AppConfig) => AppConfig) => { - setConfig(updater); - }; - - const appContextValue: AppContextType = { - config, - updateConfig, - availableRelays: RELAY_OPTIONS, - }; - - return React.createElement( - AppContext.Provider, - { value: appContextValue }, - children - ); -} - -/** - * Hook to access and update application configuration - * @returns Application context with config and update methods - */ -export function useAppConfig() { - const context = useContext(AppContext); - if (context === undefined) { - throw new Error('useAppConfig must be used within an AppProvider'); - } - return context; -} - -/** - * Hook to get and set the active theme - * @returns Theme context with theme and setTheme - */ -export function useTheme() { - const context = useContext(AppContext); - if (context === undefined) { - throw new Error('useTheme must be used within an AppProvider'); - } - return { - theme: context.config.theme, - setTheme: (theme: Theme) => context.updateConfig(config => ({ ...config, theme })), - }; -} - -/** - * Hook to apply theme changes to the document root - */ -export function useThemeEffect(theme: Theme) { - useEffect(() => { - const root = window.document.documentElement; - - root.classList.remove('light', 'dark'); - - if (theme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') - .matches - ? 'dark' - : 'light'; - - root.classList.add(systemTheme); - return; - } - - root.classList.add(theme); - }, [theme]); - - // Handle system theme changes when theme is set to "system" - useEffect(() => { - if (theme !== 'system') return; - - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - - const handleChange = () => { - const root = window.document.documentElement; - root.classList.remove('light', 'dark'); - - const systemTheme = mediaQuery.matches ? 'dark' : 'light'; - root.classList.add(systemTheme); - }; - - mediaQuery.addEventListener('change', handleChange); - return () => mediaQuery.removeEventListener('change', handleChange); - }, [theme]); -} \ No newline at end of file diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx new file mode 100644 index 0000000..eeb0703 --- /dev/null +++ b/src/components/AppProvider.tsx @@ -0,0 +1,82 @@ +import { ReactNode, useEffect } from 'react'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { AppContext, type AppConfig, type AppContextType, type Theme } from '@/contexts/AppContext'; + +interface AppProviderProps { + children: ReactNode; + /** Application storage key */ + storageKey: string; + /** Default app configuration */ + defaultConfig: AppConfig; +} + +export function AppProvider(props: AppProviderProps) { + const { + children, + storageKey, + defaultConfig + } = props; + + // App configuration state with localStorage persistence + const [config, setConfig] = useLocalStorage(storageKey, defaultConfig); + + // Generic config updater with callback pattern + const updateConfig = (updater: (currentConfig: AppConfig) => AppConfig) => { + setConfig(updater); + }; + + const appContextValue: AppContextType = { + config, + updateConfig, + }; + + // Apply theme effects to document + useApplyTheme(config.theme); + + return ( + + {children} + + ); +} + +/** + * Hook to apply theme changes to the document root + */ +function useApplyTheme(theme: Theme) { + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove('light', 'dark'); + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') + .matches + ? 'dark' + : 'light'; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + // Handle system theme changes when theme is set to "system" + useEffect(() => { + if (theme !== 'system') return; + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleChange = () => { + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + + const systemTheme = mediaQuery.matches ? 'dark' : 'light'; + root.classList.add(systemTheme); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [theme]); +} \ No newline at end of file diff --git a/src/components/NostrProvider.tsx b/src/components/NostrProvider.tsx index 6d60c2a..1de8cbb 100644 --- a/src/components/NostrProvider.tsx +++ b/src/components/NostrProvider.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react'; import { NostrEvent, NPool, NRelay1 } from '@nostrify/nostrify'; import { NostrContext } from '@nostrify/react'; import { useQueryClient } from '@tanstack/react-query'; -import { useAppConfig } from './AppProvider'; +import { useAppConfig } from '@/hooks/useAppConfig'; interface NostrProviderProps { children: React.ReactNode; @@ -10,7 +10,7 @@ interface NostrProviderProps { const NostrProvider: React.FC = (props) => { const { children } = props; - const { config, availableRelays } = useAppConfig(); + const { config } = useAppConfig(); const queryClient = useQueryClient(); @@ -19,14 +19,12 @@ const NostrProvider: React.FC = (props) => { // Use refs so the pool always has the latest data const relayUrl = useRef(config.relayUrl); - const available = useRef(availableRelays); // Update refs when config changes useEffect(() => { relayUrl.current = config.relayUrl; - available.current = availableRelays; queryClient.resetQueries(); - }, [config.relayUrl, availableRelays, queryClient]); + }, [config.relayUrl, queryClient]); // Initialize NPool only once if (!pool.current) { @@ -38,7 +36,7 @@ const NostrProvider: React.FC = (props) => { return new Map([[relayUrl.current, filters]]); }, eventRouter(_event: NostrEvent) { - return available.current.map((info) => info.url); + return [relayUrl.current]; }, }); } diff --git a/src/components/RelaySelector.tsx b/src/components/RelaySelector.tsx index ae7f4f5..7ab4b2a 100644 --- a/src/components/RelaySelector.tsx +++ b/src/components/RelaySelector.tsx @@ -14,15 +14,17 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { useAppConfig } from "@/components/AppProvider"; +import { useAppConfig } from "@/hooks/useAppConfig"; import { useState } from "react"; interface RelaySelectorProps { className?: string; + availableRelays?: { name: string; url: string }[]; } -export function RelaySelector({ className }: RelaySelectorProps) { - const { config, updateConfig, availableRelays } = useAppConfig(); +export function RelaySelector({ className, availableRelays = [] }: RelaySelectorProps) { + const { config, updateConfig } = useAppConfig(); + const [open, setOpen] = useState(false); const [inputValue, setInputValue] = useState(""); diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts new file mode 100644 index 0000000..383e52d --- /dev/null +++ b/src/contexts/AppContext.ts @@ -0,0 +1,19 @@ +import { createContext } from "react"; + +export type Theme = "dark" | "light" | "system"; + +export interface AppConfig { + /** Current theme */ + theme: Theme; + /** Selected relay URL */ + relayUrl: string; +} + +export interface AppContextType { + /** Current application configuration */ + config: AppConfig; + /** Update configuration using a callback that receives current config and returns new config */ + updateConfig: (updater: (currentConfig: AppConfig) => AppConfig) => void; +} + +export const AppContext = createContext(undefined); diff --git a/src/hooks/useAppConfig.ts b/src/hooks/useAppConfig.ts new file mode 100644 index 0000000..221fc88 --- /dev/null +++ b/src/hooks/useAppConfig.ts @@ -0,0 +1,14 @@ +import { useContext } from "react"; +import { AppContext, type AppContextType } from "@/contexts/AppContext"; + +/** + * Hook to access and update application configuration + * @returns Application context with config and update methods + */ +export function useAppConfig(): AppContextType { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error('useAppConfig must be used within an AppProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index 31c692a..320cdc2 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -1,6 +1,20 @@ -import { useTheme as useThemeFromProvider } from "@/components/AppProvider"; +import { type Theme } from "@/contexts/AppContext"; +import { useAppConfig } from "@/hooks/useAppConfig"; -/** Hook to get and set the active theme. */ -export function useTheme() { - return useThemeFromProvider(); +/** + * Hook to get and set the active theme + * @returns Theme context with theme and setTheme + */ +export function useTheme(): { theme: Theme; setTheme: (theme: Theme) => void } { + const { config, updateConfig } = useAppConfig(); + + return { + theme: config.theme, + setTheme: (theme: Theme) => { + updateConfig((currentConfig) => ({ + ...currentConfig, + theme, + })); + } + } } \ No newline at end of file diff --git a/src/test/TestApp.tsx b/src/test/TestApp.tsx index 0d7f7cf..ba48ba6 100644 --- a/src/test/TestApp.tsx +++ b/src/test/TestApp.tsx @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { NostrLoginProvider } from '@nostrify/react/login'; import NostrProvider from '@/components/NostrProvider'; import { AppProvider } from '@/components/AppProvider'; +import { AppConfig } from '@/contexts/AppContext'; interface TestAppProps { children: React.ReactNode; @@ -16,9 +17,14 @@ export function TestApp({ children }: TestAppProps) { }, }); + const defaultConfig: AppConfig = { + theme: 'light', + relayUrl: 'wss://relay.nostr.band', + }; + return ( - +