@@ -23,7 +23,6 @@ const t = {
2323 } ,
2424}
2525
26-
2726export default function ThreadDropdown ( {
2827 currentThreadId,
2928 currentTitle,
@@ -43,6 +42,8 @@ export default function ThreadDropdown({
4342 const [ loading , setLoading ] = useState ( false )
4443 const [ confirmDeleteId , setConfirmDeleteId ] = useState < string | null > ( null )
4544 const dropdownRef = useRef < HTMLDivElement > ( null )
45+ const isNavigatingRef = useRef ( false )
46+ const searchInputRef = useRef < HTMLInputElement > ( null )
4647 const router = useRouter ( )
4748 const labels = t [ locale ]
4849
@@ -76,13 +77,23 @@ export default function ThreadDropdown({
7677 }
7778 } , [ ] )
7879
80+ // Fetch on open, reset search
7981 useEffect ( ( ) => {
80- if ( isOpen ) fetchThreads ( )
82+ if ( isOpen ) {
83+ setSearch ( "" )
84+ setConfirmDeleteId ( null )
85+ fetchThreads ( )
86+ // Focus search input after render
87+ requestAnimationFrame ( ( ) => searchInputRef . current ?. focus ( ) )
88+ }
8189 } , [ isOpen , fetchThreads ] )
8290
83- // Debounced search
91+ // Debounced search (only when user types, not on open)
92+ const isFirstRender = useRef ( true )
8493 useEffect ( ( ) => {
85- if ( ! isOpen ) return
94+ if ( ! isOpen ) { isFirstRender . current = true ; return }
95+ // Skip the first render (handled by the open effect above)
96+ if ( isFirstRender . current ) { isFirstRender . current = false ; return }
8697 const timer = setTimeout ( ( ) => fetchThreads ( search || undefined ) , 300 )
8798 return ( ) => clearTimeout ( timer )
8899 } , [ search , isOpen , fetchThreads ] )
@@ -95,12 +106,22 @@ export default function ThreadDropdown({
95106 } , [ confirmDeleteId ] )
96107
97108 const handleSelect = ( threadId : string ) => {
109+ if ( threadId === currentThreadId ) {
110+ // Already viewing this thread, just close
111+ setIsOpen ( false )
112+ return
113+ }
114+ // Close immediately without animation to prevent flash during navigation
115+ isNavigatingRef . current = true
98116 setIsOpen ( false )
99117 router . push ( `/chat?thread=${ threadId } ` )
118+ // Reset navigation flag after transition
119+ setTimeout ( ( ) => { isNavigatingRef . current = false } , 500 )
100120 }
101121
102122 const handleDelete = async ( e : React . MouseEvent , threadId : string ) => {
103123 e . stopPropagation ( )
124+ e . preventDefault ( )
104125 if ( confirmDeleteId === threadId ) {
105126 try {
106127 const res = await fetch ( `/api/threads/${ threadId } ` , { method : "DELETE" } )
@@ -119,12 +140,17 @@ export default function ThreadDropdown({
119140 }
120141 }
121142
143+ const handleToggle = ( ) => {
144+ if ( isNavigatingRef . current ) return
145+ setIsOpen ( ( prev ) => ! prev )
146+ }
147+
122148 const displayTitle = currentTitle || "Quorum"
123149
124150 return (
125151 < div ref = { dropdownRef } className = "relative" >
126152 < button
127- onClick = { ( ) => setIsOpen ( ! isOpen ) }
153+ onClick = { handleToggle }
128154 aria-haspopup = "listbox"
129155 aria-expanded = { isOpen }
130156 className = "flex items-center gap-1.5 text-sm font-semibold text-[var(--foreground)] hover:text-[var(--foreground)]/80 transition-colors max-w-[200px] sm:max-w-[300px]"
@@ -133,26 +159,27 @@ export default function ThreadDropdown({
133159 < ChevronDown className = { cn ( "w-3.5 h-3.5 shrink-0 transition-transform" , isOpen && "rotate-180" ) } />
134160 </ button >
135161
136- < AnimatePresence >
162+ < AnimatePresence mode = "wait" >
137163 { isOpen && (
138164 < motion . div
165+ key = "thread-dropdown"
139166 initial = { { opacity : 0 , y : - 4 } }
140167 animate = { { opacity : 1 , y : 0 } }
141- exit = { { opacity : 0 , y : - 4 } }
142- transition = { { duration : 0.15 } }
168+ exit = { { opacity : 0 } }
169+ transition = { { duration : 0.12 } }
143170 className = "absolute top-full left-0 mt-2 w-[calc(100vw-2rem)] sm:w-80 bg-[var(--card)] border border-[var(--border)] rounded-xl shadow-xl z-50 overflow-hidden"
144171 >
145172 { /* Search */ }
146173 < div className = "p-2 border-b border-[var(--border)]" >
147174 < div className = "relative" >
148175 < Search className = "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--muted-foreground)]" />
149176 < input
177+ ref = { searchInputRef }
150178 type = "text"
151179 value = { search }
152180 onChange = { ( e ) => setSearch ( e . target . value ) }
153181 placeholder = { labels . search }
154182 className = "w-full pl-8 pr-3 py-2 sm:py-1.5 text-sm bg-transparent text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] outline-none"
155- autoFocus
156183 />
157184 </ div >
158185 </ div >
0 commit comments