@@ -153,59 +153,64 @@ function ChatPanelComponent({
153153 return lineCount || 1 ; // Ensure at least 1 line
154154 } ;
155155
156+ // Clamp selection to valid range
157+ const clampedSelection = selectedMessageIndex >= 0
158+ ? Math . min ( selectedMessageIndex , filteredMessages . length - 1 )
159+ : - 1 ;
160+
156161 // Calculate visible messages based on actual line heights
157162 const visibleMessages : DbMessage [ ] = [ ] ;
158163 let scrollOffset = 0 ;
159- let totalLines = 0 ;
160-
161- if ( selectedMessageIndex < 0 ) {
162- // No selection - show most recent messages that fit
163- let linesUsed = 0 ;
164- for ( let i = filteredMessages . length - 1 ; i >= 0 ; i -- ) {
165- const msgHeight = getMessageHeight ( filteredMessages [ i ] ) ;
166- if ( linesUsed + msgHeight <= messageAreaHeight ) {
167- visibleMessages . unshift ( filteredMessages [ i ] ) ;
168- linesUsed += msgHeight ;
169- scrollOffset = i ;
164+
165+ // Compute bottom-anchored view (what's visible when showing most recent messages)
166+ let bottomLinesUsed = 0 ;
167+ let bottomViewStart = filteredMessages . length ;
168+ for ( let i = filteredMessages . length - 1 ; i >= 0 ; i -- ) {
169+ const h = getMessageHeight ( filteredMessages [ i ] ) ;
170+ if ( bottomLinesUsed + h <= messageAreaHeight ) {
171+ bottomLinesUsed += h ;
172+ bottomViewStart = i ;
173+ } else {
174+ break ;
175+ }
176+ }
177+
178+ if ( clampedSelection < 0 || clampedSelection >= bottomViewStart ) {
179+ // No selection, or selection is within the bottom view — show bottom-anchored view
180+ for ( let i = bottomViewStart ; i < filteredMessages . length ; i ++ ) {
181+ visibleMessages . push ( filteredMessages [ i ] ) ;
182+ }
183+ scrollOffset = bottomViewStart ;
184+ } else {
185+ // Selection is above the bottom view — scroll up to keep it visible
186+ // Show selected message near the bottom third with context above
187+ visibleMessages . push ( filteredMessages [ clampedSelection ] ) ;
188+ let linesUsed = getMessageHeight ( filteredMessages [ clampedSelection ] ) ;
189+ scrollOffset = clampedSelection ;
190+
191+ // Fill below (newer messages for context, up to ~1/3 of viewport)
192+ const maxBelowLines = Math . floor ( messageAreaHeight / 3 ) ;
193+ let belowLines = 0 ;
194+ for ( let i = clampedSelection + 1 ; i < filteredMessages . length ; i ++ ) {
195+ const h = getMessageHeight ( filteredMessages [ i ] ) ;
196+ if ( belowLines + h <= maxBelowLines && linesUsed + h <= messageAreaHeight ) {
197+ visibleMessages . push ( filteredMessages [ i ] ) ;
198+ belowLines += h ;
199+ linesUsed += h ;
170200 } else {
171201 break ;
172202 }
173203 }
174- } else {
175- // Try to center the selected message
176- scrollOffset = Math . max ( 0 , selectedMessageIndex ) ;
177- let linesUsed = 0 ;
178-
179- // Add selected message first
180- if ( scrollOffset < filteredMessages . length ) {
181- visibleMessages . push ( filteredMessages [ scrollOffset ] ) ;
182- linesUsed += getMessageHeight ( filteredMessages [ scrollOffset ] ) ;
183- }
184204
185- // Add messages before and after alternately to center
186- let before = scrollOffset - 1 ;
187- let after = scrollOffset + 1 ;
188- while ( ( before >= 0 || after < filteredMessages . length ) && linesUsed < messageAreaHeight ) {
189- if ( after < filteredMessages . length ) {
190- const msgHeight = getMessageHeight ( filteredMessages [ after ] ) ;
191- if ( linesUsed + msgHeight <= messageAreaHeight ) {
192- visibleMessages . push ( filteredMessages [ after ] ) ;
193- linesUsed += msgHeight ;
194- after ++ ;
195- } else {
196- break ;
197- }
198- }
199- if ( before >= 0 ) {
200- const msgHeight = getMessageHeight ( filteredMessages [ before ] ) ;
201- if ( linesUsed + msgHeight <= messageAreaHeight ) {
202- visibleMessages . unshift ( filteredMessages [ before ] ) ;
203- linesUsed += msgHeight ;
204- scrollOffset = before ;
205- before -- ;
206- } else if ( after >= filteredMessages . length ) {
207- break ;
208- }
205+ // Fill above with remaining space
206+ for ( let i = clampedSelection - 1 ; i >= 0 ; i -- ) {
207+ const h = getMessageHeight ( filteredMessages [ i ] ) ;
208+ if ( linesUsed + h <= messageAreaHeight ) {
209+ visibleMessages . unshift ( filteredMessages [ i ] ) ;
210+ linesUsed += h ;
211+ scrollOffset = i ;
212+ } else {
213+ break ;
209214 }
210215 }
211216 }
@@ -304,7 +309,7 @@ function ChatPanelComponent({
304309 message = { msg }
305310 nodeStore = { nodeStore }
306311 isOwn = { msg . fromNode === myNodeNum }
307- isSelected = { actualIndex === selectedMessageIndex && ! inputFocused }
312+ isSelected = { actualIndex === clampedSelection && ! inputFocused }
308313 width = { width }
309314 meshViewConfirmedIds = { meshViewConfirmedIds }
310315 allMessages = { messages }
0 commit comments