Refactor AppProvider, AppConfig and presetRelays

This commit is contained in:
Alex Gleason 2025-06-04 10:20:10 -05:00
parent 3ad83cd114
commit 048bf4cc69
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
8 changed files with 46 additions and 26 deletions

View File

@ -27,9 +27,16 @@ const defaultConfig: AppConfig = {
relayUrl: "wss://relay.nostr.band", relayUrl: "wss://relay.nostr.band",
}; };
const presetRelays = [
{ 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' },
];
export function App() { export function App() {
return ( return (
<AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig}> <AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig} presetRelays={presetRelays}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<NostrLoginProvider storageKey='nostr:login'> <NostrLoginProvider storageKey='nostr:login'>
<NostrProvider> <NostrProvider>

View File

@ -8,13 +8,16 @@ interface AppProviderProps {
storageKey: string; storageKey: string;
/** Default app configuration */ /** Default app configuration */
defaultConfig: AppConfig; defaultConfig: AppConfig;
/** Optional list of preset relays to display in the RelaySelector */
presetRelays?: { name: string; url: string }[];
} }
export function AppProvider(props: AppProviderProps) { export function AppProvider(props: AppProviderProps) {
const { const {
children, children,
storageKey, storageKey,
defaultConfig defaultConfig,
presetRelays,
} = props; } = props;
// App configuration state with localStorage persistence // App configuration state with localStorage persistence
@ -28,6 +31,7 @@ export function AppProvider(props: AppProviderProps) {
const appContextValue: AppContextType = { const appContextValue: AppContextType = {
config, config,
updateConfig, updateConfig,
presetRelays,
}; };
// Apply theme effects to document // Apply theme effects to document

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 '@/hooks/useAppConfig'; import { useAppContext } from '@/hooks/useAppContext';
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 } = useAppConfig(); const { config } = useAppContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@ -14,21 +14,22 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
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 }[]; selectedRelay?: string;
setSelectedRelay: (relay: string) => void;
presetRelays?: { name: string; url: string }[];
} }
export function RelaySelector({ className, availableRelays = [] }: RelaySelectorProps) { export function RelaySelector(props: RelaySelectorProps) {
const { config, updateConfig } = useAppConfig(); const { selectedRelay, setSelectedRelay, className, presetRelays = [] } = props;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const selectedOption = availableRelays.find((option) => option.url === config.relayUrl); const selectedOption = presetRelays.find((option) => option.url === selectedRelay);
// Function to normalize relay URL by adding wss:// if no protocol is present // Function to normalize relay URL by adding wss:// if no protocol is present
const normalizeRelayUrl = (url: string): string => { const normalizeRelayUrl = (url: string): string => {
@ -46,12 +47,9 @@ export function RelaySelector({ className, availableRelays = [] }: RelaySelector
// Handle adding a custom relay // Handle adding a custom relay
const handleAddCustomRelay = (url: string) => { const handleAddCustomRelay = (url: string) => {
const normalizedUrl = normalizeRelayUrl(url); setSelectedRelay?.(normalizeRelayUrl(url));
if (normalizedUrl) { setOpen(false);
updateConfig(config => ({ ...config, relayUrl: normalizedUrl })); setInputValue("");
setOpen(false);
setInputValue("");
}
}; };
// Check if input value looks like a valid relay URL // Check if input value looks like a valid relay URL
@ -83,8 +81,8 @@ export function RelaySelector({ className, availableRelays = [] }: RelaySelector
<span className="truncate"> <span className="truncate">
{selectedOption {selectedOption
? selectedOption.name ? selectedOption.name
: config.relayUrl : selectedRelay
? config.relayUrl.replace(/^wss?:\/\//, '') ? selectedRelay.replace(/^wss?:\/\//, '')
: "Select relay..." : "Select relay..."
} }
</span> </span>
@ -121,7 +119,7 @@ export function RelaySelector({ className, availableRelays = [] }: RelaySelector
)} )}
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
{availableRelays {presetRelays
.filter((option) => .filter((option) =>
!inputValue || !inputValue ||
option.name.toLowerCase().includes(inputValue.toLowerCase()) || option.name.toLowerCase().includes(inputValue.toLowerCase()) ||
@ -132,7 +130,7 @@ export function RelaySelector({ className, availableRelays = [] }: RelaySelector
key={option.url} key={option.url}
value={option.url} value={option.url}
onSelect={(currentValue) => { onSelect={(currentValue) => {
updateConfig(config => ({ ...config, relayUrl: currentValue })); setSelectedRelay(normalizeRelayUrl(currentValue));
setOpen(false); setOpen(false);
setInputValue(""); setInputValue("");
}} }}
@ -140,7 +138,7 @@ export function RelaySelector({ className, availableRelays = [] }: RelaySelector
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
config.relayUrl === option.url ? "opacity-100" : "opacity-0" selectedRelay === option.url ? "opacity-100" : "opacity-0"
)} )}
/> />
<div className="flex flex-col"> <div className="flex flex-col">

View File

@ -12,6 +12,7 @@ import {
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.tsx'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.tsx';
import { RelaySelector } from '@/components/RelaySelector'; import { RelaySelector } from '@/components/RelaySelector';
import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts'; import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts';
import { useAppContext } from '@/hooks/useAppContext';
import { genUserName } from '@/lib/genUserName'; import { genUserName } from '@/lib/genUserName';
interface AccountSwitcherProps { interface AccountSwitcherProps {
@ -19,11 +20,14 @@ interface AccountSwitcherProps {
} }
export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) { export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) {
const { config, updateConfig, presetRelays } = useAppContext();
const { currentUser, otherUsers, setLogin, removeLogin } = useLoggedInAccounts(); const { currentUser, otherUsers, setLogin, removeLogin } = useLoggedInAccounts();
if (!currentUser) return null; if (!currentUser) return null;
const getDisplayName = (account: Account): string => account.metadata.name ?? genUserName(account.pubkey); const getDisplayName = (account: Account): string => {
return account.metadata.name ?? genUserName(account.pubkey);
}
return ( return (
<DropdownMenu> <DropdownMenu>
@ -41,7 +45,12 @@ export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className='w-56 p-2 animate-scale-in'> <DropdownMenuContent className='w-56 p-2 animate-scale-in'>
<div className='font-medium text-sm px-2 py-1.5'>Switch Relay</div> <div className='font-medium text-sm px-2 py-1.5'>Switch Relay</div>
<RelaySelector className="w-full" /> <RelaySelector
className="w-full"
selectedRelay={config.relayUrl}
setSelectedRelay={(relayUrl) => updateConfig((config) => ({ ...config, relayUrl }))}
presetRelays={presetRelays}
/>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className='font-medium text-sm px-2 py-1.5'>Switch Account</div> <div className='font-medium text-sm px-2 py-1.5'>Switch Account</div>
{otherUsers.map((user) => ( {otherUsers.map((user) => (

View File

@ -14,6 +14,8 @@ export interface AppContextType {
config: AppConfig; config: AppConfig;
/** Update configuration using a callback that receives current config and returns new config */ /** Update configuration using a callback that receives current config and returns new config */
updateConfig: (updater: (currentConfig: AppConfig) => AppConfig) => void; updateConfig: (updater: (currentConfig: AppConfig) => AppConfig) => void;
/** Optional list of preset relays to display in the RelaySelector */
presetRelays?: { name: string; url: string }[];
} }
export const AppContext = createContext<AppContextType | undefined>(undefined); export const AppContext = createContext<AppContextType | undefined>(undefined);

View File

@ -5,10 +5,10 @@ import { AppContext, type AppContextType } from "@/contexts/AppContext";
* Hook to access and update application configuration * Hook to access and update application configuration
* @returns Application context with config and update methods * @returns Application context with config and update methods
*/ */
export function useAppConfig(): AppContextType { export function useAppContext(): AppContextType {
const context = useContext(AppContext); const context = useContext(AppContext);
if (context === undefined) { if (context === undefined) {
throw new Error('useAppConfig must be used within an AppProvider'); throw new Error('useAppContext must be used within an AppProvider');
} }
return context; return context;
} }

View File

@ -1,12 +1,12 @@
import { type Theme } from "@/contexts/AppContext"; import { type Theme } from "@/contexts/AppContext";
import { useAppConfig } from "@/hooks/useAppConfig"; import { useAppContext } from "@/hooks/useAppContext";
/** /**
* Hook to get and set the active theme * Hook to get and set the active theme
* @returns Theme context with theme and setTheme * @returns Theme context with theme and setTheme
*/ */
export function useTheme(): { theme: Theme; setTheme: (theme: Theme) => void } { export function useTheme(): { theme: Theme; setTheme: (theme: Theme) => void } {
const { config, updateConfig } = useAppConfig(); const { config, updateConfig } = useAppContext();
return { return {
theme: config.theme, theme: config.theme,