mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 13:09:22 +00:00
Refactor AppProvider
This commit is contained in:
parent
c3310ab485
commit
3ad83cd114
@ -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 (
|
||||
<AppProvider defaultTheme="light">
|
||||
<AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NostrLoginProvider storageKey='nostr:login'>
|
||||
<NostrProvider>
|
||||
|
@ -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]);
|
||||
}
|
82
src/components/AppProvider.tsx
Normal file
82
src/components/AppProvider.tsx
Normal 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]);
|
||||
}
|
@ -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<NostrProviderProps> = (props) => {
|
||||
const { children } = props;
|
||||
const { config, availableRelays } = useAppConfig();
|
||||
const { config } = useAppConfig();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@ -19,14 +19,12 @@ const NostrProvider: React.FC<NostrProviderProps> = (props) => {
|
||||
|
||||
// Use refs so the pool always has the latest data
|
||||
const relayUrl = useRef<string>(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<NostrProviderProps> = (props) => {
|
||||
return new Map([[relayUrl.current, filters]]);
|
||||
},
|
||||
eventRouter(_event: NostrEvent) {
|
||||
return available.current.map((info) => info.url);
|
||||
return [relayUrl.current];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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("");
|
||||
|
||||
|
19
src/contexts/AppContext.ts
Normal file
19
src/contexts/AppContext.ts
Normal 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
14
src/hooks/useAppConfig.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<BrowserRouter>
|
||||
<AppProvider>
|
||||
<AppProvider storageKey='test-app-config' defaultConfig={defaultConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NostrLoginProvider storageKey='test-login'>
|
||||
<NostrProvider>
|
||||
|
Loading…
x
Reference in New Issue
Block a user