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 { 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>
|
||||||
|
@ -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 { 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];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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("");
|
||||||
|
|
||||||
|
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() {
|
* 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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user