Refactor AppProvider

This commit is contained in:
Alex Gleason 2025-06-04 10:04:39 -05:00
parent c3310ab485
commit 3ad83cd114
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
9 changed files with 156 additions and 204 deletions

View File

@ -9,6 +9,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { NostrLoginProvider } from '@nostrify/react/login'; import { NostrLoginProvider } from '@nostrify/react/login';
import { AppProvider } from '@/components/AppProvider'; import { AppProvider } from '@/components/AppProvider';
import { AppConfig } from '@/contexts/AppContext';
import AppRouter from './AppRouter'; import AppRouter from './AppRouter';
const queryClient = new QueryClient({ 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() { export function App() {
return ( return (
<AppProvider defaultTheme="light"> <AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<NostrLoginProvider storageKey='nostr:login'> <NostrLoginProvider storageKey='nostr:login'>
<NostrProvider> <NostrProvider>

View File

@ -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<AppContextType | undefined>(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]);
}

View File

@ -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<AppConfig>(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 (
<AppContext.Provider value={appContextValue}>
{children}
</AppContext.Provider>
);
}
/**
* 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]);
}

View File

@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react';
import { NostrEvent, NPool, NRelay1 } from '@nostrify/nostrify'; import { NostrEvent, NPool, NRelay1 } from '@nostrify/nostrify';
import { NostrContext } from '@nostrify/react'; import { NostrContext } from '@nostrify/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useAppConfig } from './AppProvider'; import { useAppConfig } from '@/hooks/useAppConfig';
interface NostrProviderProps { interface NostrProviderProps {
children: React.ReactNode; children: React.ReactNode;
@ -10,7 +10,7 @@ interface NostrProviderProps {
const NostrProvider: React.FC<NostrProviderProps> = (props) => { const NostrProvider: React.FC<NostrProviderProps> = (props) => {
const { children } = props; const { children } = props;
const { config, availableRelays } = useAppConfig(); const { config } = useAppConfig();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -19,14 +19,12 @@ const NostrProvider: React.FC<NostrProviderProps> = (props) => {
// Use refs so the pool always has the latest data // Use refs so the pool always has the latest data
const relayUrl = useRef<string>(config.relayUrl); const relayUrl = useRef<string>(config.relayUrl);
const available = useRef(availableRelays);
// Update refs when config changes // Update refs when config changes
useEffect(() => { useEffect(() => {
relayUrl.current = config.relayUrl; relayUrl.current = config.relayUrl;
available.current = availableRelays;
queryClient.resetQueries(); queryClient.resetQueries();
}, [config.relayUrl, availableRelays, queryClient]); }, [config.relayUrl, queryClient]);
// Initialize NPool only once // Initialize NPool only once
if (!pool.current) { if (!pool.current) {
@ -38,7 +36,7 @@ const NostrProvider: React.FC<NostrProviderProps> = (props) => {
return new Map([[relayUrl.current, filters]]); return new Map([[relayUrl.current, filters]]);
}, },
eventRouter(_event: NostrEvent) { eventRouter(_event: NostrEvent) {
return available.current.map((info) => info.url); return [relayUrl.current];
}, },
}); });
} }

View File

@ -14,15 +14,17 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { useAppConfig } from "@/components/AppProvider"; import { useAppConfig } from "@/hooks/useAppConfig";
import { useState } from "react"; import { useState } from "react";
interface RelaySelectorProps { interface RelaySelectorProps {
className?: string; className?: string;
availableRelays?: { name: string; url: string }[];
} }
export function RelaySelector({ className }: RelaySelectorProps) { export function RelaySelector({ className, availableRelays = [] }: RelaySelectorProps) {
const { config, updateConfig, availableRelays } = useAppConfig(); const { config, updateConfig } = useAppConfig();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");

View File

@ -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<AppContextType | undefined>(undefined);

14
src/hooks/useAppConfig.ts Normal file
View File

@ -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;
}

View File

@ -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() { * Hook to get and set the active theme
return useThemeFromProvider(); * @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,
}));
}
}
} }

View File

@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { NostrLoginProvider } from '@nostrify/react/login'; import { NostrLoginProvider } from '@nostrify/react/login';
import NostrProvider from '@/components/NostrProvider'; import NostrProvider from '@/components/NostrProvider';
import { AppProvider } from '@/components/AppProvider'; import { AppProvider } from '@/components/AppProvider';
import { AppConfig } from '@/contexts/AppContext';
interface TestAppProps { interface TestAppProps {
children: React.ReactNode; children: React.ReactNode;
@ -16,9 +17,14 @@ export function TestApp({ children }: TestAppProps) {
}, },
}); });
const defaultConfig: AppConfig = {
theme: 'light',
relayUrl: 'wss://relay.nostr.band',
};
return ( return (
<BrowserRouter> <BrowserRouter>
<AppProvider> <AppProvider storageKey='test-app-config' defaultConfig={defaultConfig}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<NostrLoginProvider storageKey='test-login'> <NostrLoginProvider storageKey='test-login'>
<NostrProvider> <NostrProvider>