@@ -91,6 +91,8 @@ class ChatScrollCoordinator {
9191 final _renderedMessageIds = < String > {};
9292 bool _tailFollowEligible = true ;
9393 _TailFollowAnchorSnapshot ? _tailFollowAnchorSnapshot;
94+ bool _tailFollowMessageWindowChanged = false ;
95+ _ViewportAnchorSnapshot ? _viewportAnchorSnapshot;
9496 bool _animateNextRestore = false ;
9597 ChatScrollRestoreDirection ? _animatedRestoreDirection;
9698 bool _restoreScheduled = false ;
@@ -111,15 +113,25 @@ class ChatScrollCoordinator {
111113 Map <String , GlobalKey > keysByMessageId,
112114 ) {
113115 if (! scrollController.hasClients) return ;
116+ final messageWindowChanged = ! _hasSameMessageIds (messages);
114117 _setMessages (messages, keysByMessageId);
115118 _tailFollowEligible = _isTailFollowEligible ();
116119 _tailFollowAnchorSnapshot = _tailFollowEligible
117120 ? _tailFollowAnchorSnapshotFromViewport ()
118121 : null ;
122+ _tailFollowMessageWindowChanged =
123+ _tailFollowEligible &&
124+ messageWindowChanged &&
125+ _tailFollowAnchorSnapshot != null ;
126+ _viewportAnchorSnapshot = _tailFollowEligible
127+ ? null
128+ : _viewportAnchorSnapshotFromViewport ();
119129 traceChatScroll (
120130 'capture tailEligible=$_tailFollowEligible '
121131 'anchor=${shortMessageId (_tailFollowAnchorSnapshot ?.messageId )} '
122132 'anchorBottom=${formatDouble (_tailFollowAnchorSnapshot ?.bottom )} '
133+ 'viewportAnchor=${shortMessageId (_viewportAnchorSnapshot ?.messageId )} '
134+ 'viewportTop=${formatDouble (_viewportAnchorSnapshot ?.top )} '
123135 '${_formatScrollState ()}' ,
124136 );
125137 }
@@ -189,6 +201,9 @@ class ChatScrollCoordinator {
189201 _animateNextRestore ||
190202 animateLatestReset);
191203 if (reset) {
204+ _tailFollowAnchorSnapshot = null ;
205+ _tailFollowMessageWindowChanged = false ;
206+ _viewportAnchorSnapshot = null ;
192207 _animatedRestoreMessageId = null ;
193208 _animatedRestoreDirection = null ;
194209 _animatedRestoreComplete = null ;
@@ -371,6 +386,38 @@ class ChatScrollCoordinator {
371386 return true ;
372387 }
373388
389+ double ? takeViewportAnchorCorrection () {
390+ final snapshot = _viewportAnchorSnapshot;
391+ _viewportAnchorSnapshot = null ;
392+ if (snapshot == null || ! scrollController.hasClients) return null ;
393+ final top = _messageTopInViewport (
394+ snapshot.messageId,
395+ _keysByMessageId,
396+ );
397+ if (top == null ) {
398+ traceChatJump (
399+ 'viewport-anchor missing anchor=${shortMessageId (snapshot .messageId )} '
400+ '${formatScrollMetrics (scrollController .position )}' ,
401+ );
402+ return null ;
403+ }
404+
405+ final delta = top - snapshot.top;
406+ if (delta.abs () <= _jumpToTolerance) return null ;
407+ traceChatJump (
408+ 'viewport-anchor correction anchor=${shortMessageId (snapshot .messageId )} '
409+ 'delta=${formatDouble (delta )} ${formatScrollMetrics (scrollController .position )}' ,
410+ );
411+ traceChatScroll (
412+ 'viewport-anchor correction anchor=${shortMessageId (snapshot .messageId )} '
413+ 'delta=${formatDouble (delta )} ${_formatScrollState ()}' ,
414+ );
415+ return delta;
416+ }
417+
418+ bool shouldSuppressAutoBottomTracking () =>
419+ _tailFollowMessageWindowChanged && _tailFollowAnchorSnapshot != null ;
420+
374421 void dispose () {
375422 _disposed = true ;
376423 scrollController
@@ -483,9 +530,14 @@ class ChatScrollCoordinator {
483530 final render = _messageRender (messageId, keysByMessageId);
484531 final viewport = _viewportRender;
485532 if (render == null || viewport == null ) return null ;
533+ if (! render.hasSize || ! viewport.hasSize) return null ;
534+
535+ final viewportOrigin = _globalOrigin (viewport);
536+ final renderOrigin = _globalOrigin (render);
537+ if (viewportOrigin == null || renderOrigin == null ) return null ;
486538
487- final viewportTop = viewport. localToGlobal ( Offset .zero) .dy;
488- final top = render. localToGlobal ( Offset .zero) .dy - viewportTop;
539+ final viewportTop = viewportOrigin .dy;
540+ final top = renderOrigin .dy - viewportTop;
489541 final height = render.size.height;
490542 final target =
491543 scrollController.offset +
@@ -499,6 +551,34 @@ class ChatScrollCoordinator {
499551 );
500552 }
501553
554+ double ? _messageTopInViewport (
555+ String messageId,
556+ Map <String , GlobalKey > keysByMessageId,
557+ ) {
558+ final render = _messageRender (messageId, keysByMessageId);
559+ final viewport = _viewportRender;
560+ if (render == null || viewport == null ) return null ;
561+ if (! render.hasSize || ! viewport.hasSize) return null ;
562+
563+ final viewportOrigin = _globalOrigin (viewport);
564+ final renderOrigin = _globalOrigin (render);
565+ if (viewportOrigin == null || renderOrigin == null ) return null ;
566+
567+ return renderOrigin.dy - viewportOrigin.dy;
568+ }
569+
570+ Offset ? _globalOrigin (RenderBox render) {
571+ try {
572+ return render.localToGlobal (Offset .zero);
573+ } catch (error) {
574+ if (error is StateError &&
575+ error.message.contains ('RenderBox was not laid out' )) {
576+ return null ;
577+ }
578+ rethrow ;
579+ }
580+ }
581+
502582 bool _isTargetInLoadedJumpWindow (double target) {
503583 if (! scrollController.hasClients) return false ;
504584 final position = scrollController.position;
@@ -745,9 +825,30 @@ class ChatScrollCoordinator {
745825 return snapshot;
746826 }
747827
828+ _ViewportAnchorSnapshot ? _viewportAnchorSnapshotFromViewport () {
829+ _ViewportAnchorSnapshot ? snapshot;
830+ final viewportHeight = scrollController.position.viewportDimension;
831+ for (final messageId in _currentRenderedMessageIds ()) {
832+ final geometry = _messageTargetGeometry (messageId, _keysByMessageId);
833+ if (geometry == null ||
834+ geometry.bottom <= 0 ||
835+ geometry.top >= viewportHeight) {
836+ continue ;
837+ }
838+ if (snapshot == null || geometry.top < snapshot.top) {
839+ snapshot = _ViewportAnchorSnapshot (
840+ messageId: messageId,
841+ top: geometry.top,
842+ );
843+ }
844+ }
845+ return snapshot;
846+ }
847+
748848 void _restoreTailFollowAnimationStart () {
749849 final snapshot = _tailFollowAnchorSnapshot;
750850 _tailFollowAnchorSnapshot = null ;
851+ _tailFollowMessageWindowChanged = false ;
751852 if (snapshot == null || ! scrollController.hasClients) return ;
752853 final geometry = _messageTargetGeometry (
753854 snapshot.messageId,
@@ -985,6 +1086,16 @@ class _TailFollowAnchorSnapshot {
9851086 final double bottom;
9861087}
9871088
1089+ class _ViewportAnchorSnapshot {
1090+ const _ViewportAnchorSnapshot ({
1091+ required this .messageId,
1092+ required this .top,
1093+ });
1094+
1095+ final String messageId;
1096+ final double top;
1097+ }
1098+
9881099class _MessageTargetGeometry {
9891100 const _MessageTargetGeometry ({
9901101 required this .target,
0 commit comments