1+ import { useState } from 'react' ;
2+ import { Check , ChevronsUpDown , Wifi , Plus , RotateCcw } from "lucide-react" ;
3+ import { cn } from "@/lib/utils" ;
4+ import { Button } from "@/components/ui/button" ;
5+ import {
6+ Command ,
7+ CommandEmpty ,
8+ CommandGroup ,
9+ CommandInput ,
10+ CommandItem ,
11+ CommandList ,
12+ } from "@/components/ui/command" ;
13+ import {
14+ Popover ,
15+ PopoverContent ,
16+ PopoverTrigger ,
17+ } from "@/components/ui/popover" ;
18+ import { useAppContext } from "@/hooks/useAppContext" ;
19+
20+ interface NotesRelaySelectorProps {
21+ selectedRelay ?: string ;
22+ onSelectionChange : ( relay ?: string ) => void ;
23+ className ?: string ;
24+ placeholder ?: string ;
25+ }
26+
27+ export function NotesRelaySelector ( props : NotesRelaySelectorProps ) {
28+ const {
29+ selectedRelay,
30+ onSelectionChange,
31+ className,
32+ placeholder = "Choose a relay to search..."
33+ } = props ;
34+
35+ const { config, presetRelays = [ ] } = useAppContext ( ) ;
36+ const [ open , setOpen ] = useState ( false ) ;
37+ const [ inputValue , setInputValue ] = useState ( "" ) ;
38+
39+ // Function to normalize relay URL by adding wss:// if no protocol is present
40+ const normalizeRelayUrl = ( url : string ) : string => {
41+ const trimmed = url . trim ( ) ;
42+ if ( ! trimmed ) return trimmed ;
43+
44+ // Check if it already has a protocol
45+ if ( trimmed . includes ( '://' ) ) {
46+ return trimmed ;
47+ }
48+
49+ // Add wss:// prefix
50+ return `wss://${ trimmed } ` ;
51+ } ;
52+
53+ // Check if input value looks like a valid relay URL
54+ const isValidRelayInput = ( value : string ) : boolean => {
55+ const trimmed = value . trim ( ) ;
56+ if ( ! trimmed ) return false ;
57+
58+ // Basic validation - should contain at least a domain-like structure
59+ const normalized = normalizeRelayUrl ( trimmed ) ;
60+ try {
61+ new URL ( normalized ) ;
62+ return true ;
63+ } catch {
64+ return false ;
65+ }
66+ } ;
67+
68+ // Handle selecting a relay
69+ const handleSelectRelay = ( url : string ) => {
70+ const normalizedUrl = normalizeRelayUrl ( url ) ;
71+ onSelectionChange ( normalizedUrl ) ;
72+ setInputValue ( "" ) ;
73+ setOpen ( false ) ;
74+ } ;
75+
76+ // Handle resetting to default (current relay)
77+ const handleReset = ( ) => {
78+ onSelectionChange ( undefined ) ;
79+ setOpen ( false ) ;
80+ } ;
81+
82+ // Get available preset relays (including current relay)
83+ const availablePresetRelays = presetRelays . filter ( relay =>
84+ ! inputValue ||
85+ relay . name . toLowerCase ( ) . includes ( inputValue . toLowerCase ( ) ) ||
86+ relay . url . toLowerCase ( ) . includes ( inputValue . toLowerCase ( ) )
87+ ) ;
88+
89+ const getRelayDisplayName = ( url : string ) : string => {
90+ const preset = presetRelays . find ( relay => relay . url === url ) ;
91+ return preset ? preset . name : url . replace ( / ^ w s s ? : \/ \/ / , '' ) ;
92+ } ;
93+
94+ const currentRelayName = getRelayDisplayName ( config . relayUrl ) ;
95+ const selectedRelayName = selectedRelay ? getRelayDisplayName ( selectedRelay ) : currentRelayName ;
96+ const isUsingCustomRelay = selectedRelay && selectedRelay !== config . relayUrl ;
97+
98+ return (
99+ < div className = { cn ( "space-y-3" , className ) } >
100+ { /* Current selection display */ }
101+ < div className = "flex items-center justify-between" >
102+ < div className = "flex items-center gap-2" >
103+ < Wifi className = "h-4 w-4 text-muted-foreground" />
104+ < span className = "text-sm" >
105+ < span className = "font-medium" > Searching:</ span > { ' ' }
106+ < span className = { isUsingCustomRelay ? "text-blue-600 font-medium" : "text-muted-foreground" } >
107+ { selectedRelayName }
108+ </ span >
109+ { ! isUsingCustomRelay && (
110+ < span className = "text-xs text-muted-foreground ml-1" > (default)</ span >
111+ ) }
112+ </ span >
113+ </ div >
114+ { isUsingCustomRelay && (
115+ < Button
116+ variant = "ghost"
117+ size = "sm"
118+ onClick = { handleReset }
119+ className = "h-auto p-1"
120+ title = "Reset to default relay"
121+ >
122+ < RotateCcw className = "h-3 w-3" />
123+ </ Button >
124+ ) }
125+ </ div >
126+
127+ { /* Relay selector */ }
128+ < Popover open = { open } onOpenChange = { setOpen } >
129+ < PopoverTrigger asChild >
130+ < Button
131+ variant = "outline"
132+ role = "combobox"
133+ aria-expanded = { open }
134+ className = "w-full justify-between"
135+ >
136+ < div className = "flex items-center gap-2" >
137+ < Wifi className = "h-4 w-4" />
138+ < span className = "truncate" > { placeholder } </ span >
139+ </ div >
140+ < ChevronsUpDown className = "ml-2 h-4 w-4 shrink-0 opacity-50" />
141+ </ Button >
142+ </ PopoverTrigger >
143+ < PopoverContent className = "w-[400px] p-0" >
144+ < Command >
145+ < CommandInput
146+ placeholder = "Search relays or type URL..."
147+ value = { inputValue }
148+ onValueChange = { setInputValue }
149+ />
150+ < CommandList >
151+ < CommandEmpty >
152+ { inputValue && isValidRelayInput ( inputValue ) ? (
153+ < CommandItem
154+ onSelect = { ( ) => handleSelectRelay ( inputValue ) }
155+ className = "cursor-pointer"
156+ >
157+ < Plus className = "mr-2 h-4 w-4" />
158+ < div className = "flex flex-col" >
159+ < span className = "font-medium" > Use custom relay</ span >
160+ < span className = "text-xs text-muted-foreground" >
161+ { normalizeRelayUrl ( inputValue ) }
162+ </ span >
163+ </ div >
164+ </ CommandItem >
165+ ) : (
166+ < div className = "py-6 text-center text-sm text-muted-foreground" >
167+ { inputValue ? "Invalid relay URL" : "No relay found." }
168+ </ div >
169+ ) }
170+ </ CommandEmpty >
171+
172+ { availablePresetRelays . length > 0 && (
173+ < CommandGroup heading = "Available Relays" >
174+ { availablePresetRelays . map ( ( relay ) => (
175+ < CommandItem
176+ key = { relay . url }
177+ value = { relay . url }
178+ onSelect = { ( ) => handleSelectRelay ( relay . url ) }
179+ >
180+ < Check
181+ className = { cn (
182+ "mr-2 h-4 w-4" ,
183+ selectedRelay === relay . url ? "opacity-100" : "opacity-0"
184+ ) }
185+ />
186+ < div className = "flex flex-col" >
187+ < span className = "font-medium" > { relay . name } </ span >
188+ < span className = "text-xs text-muted-foreground" > { relay . url } </ span >
189+ { relay . url === config . relayUrl && (
190+ < span className = "text-xs text-blue-600" > Current default</ span >
191+ ) }
192+ </ div >
193+ </ CommandItem >
194+ ) ) }
195+ </ CommandGroup >
196+ ) }
197+
198+ { inputValue && isValidRelayInput ( inputValue ) && (
199+ < CommandGroup heading = "Custom Relay" >
200+ < CommandItem
201+ onSelect = { ( ) => handleSelectRelay ( inputValue ) }
202+ className = "cursor-pointer"
203+ >
204+ < Plus className = "mr-2 h-4 w-4" />
205+ < div className = "flex flex-col" >
206+ < span className = "font-medium" > Use custom relay</ span >
207+ < span className = "text-xs text-muted-foreground" >
208+ { normalizeRelayUrl ( inputValue ) }
209+ </ span >
210+ </ div >
211+ </ CommandItem >
212+ </ CommandGroup >
213+ ) }
214+ </ CommandList >
215+ </ Command >
216+ </ PopoverContent >
217+ </ Popover >
218+
219+ < div className = "text-xs text-muted-foreground" >
220+ Choose a specific relay to search for your notes. Different relays may have different historical data.
221+ </ div >
222+ </ div >
223+ ) ;
224+ }
0 commit comments