diff --git a/lib/ui/home/chat/chat_scroll_coordinator.dart b/lib/ui/home/chat/chat_scroll_coordinator.dart index 01d323655d..aedb68018f 100644 --- a/lib/ui/home/chat/chat_scroll_coordinator.dart +++ b/lib/ui/home/chat/chat_scroll_coordinator.dart @@ -201,9 +201,7 @@ class ChatScrollCoordinator { _animateNextRestore || animateLatestReset); if (reset) { - _tailFollowAnchorSnapshot = null; - _tailFollowMessageWindowChanged = false; - _viewportAnchorSnapshot = null; + _cancelProgrammaticScrollBeforeReset(); _animatedRestoreMessageId = null; _animatedRestoreDirection = null; _animatedRestoreComplete = null; @@ -240,6 +238,21 @@ class ChatScrollCoordinator { }); } + void _cancelProgrammaticScrollBeforeReset() { + _followTailAfterProgrammaticScroll = false; + _tailFollowAnchorSnapshot = null; + _tailFollowMessageWindowChanged = false; + _viewportAnchorSnapshot = null; + if (!scrollController.hasClients) return; + if (_programmaticScrollDepth <= 0 && _programmaticBottomScrollDepth <= 0) { + return; + } + final position = scrollController.position; + if (!position.isScrollingNotifier.value) return; + traceChatJump('cancel programmatic scroll before reset'); + scrollController.jumpTo(position.pixels); + } + void _resetCenteredOffsetBeforePaint() { if (!scrollController.hasClients) return; if (scrollController.offset.abs() <= _jumpToTolerance) return; diff --git a/test/ui/home/chat/chat_scroll_coordinator_test.dart b/test/ui/home/chat/chat_scroll_coordinator_test.dart index c9513919b0..7dd897169a 100644 --- a/test/ui/home/chat/chat_scroll_coordinator_test.dart +++ b/test/ui/home/chat/chat_scroll_coordinator_test.dart @@ -663,6 +663,57 @@ void main() { ); }); + testWidgets('reset restore cancels pending latest animation', (tester) async { + final coordinator = ChatScrollCoordinator(); + final messages = List.generate(30, testMessage); + final top = List.generate(8, testMessage); + final bottom = List.generate(8, (index) => testMessage(index + 100)); + final nextMessages = [...top, ...bottom]; + final keysByMessageId = { + for (final message in [...messages, ...nextMessages]) + message.messageId: GlobalKey(), + }; + final centerKey = GlobalKey(); + + addTearDown(coordinator.dispose); + await pumpFullyBuiltScrollableMessages( + tester, + coordinator, + messages, + keysByMessageId, + ); + coordinator.scrollController.jumpTo( + coordinator.scrollController.position.maxScrollExtent - 120, + ); + + unawaited(coordinator.scrollToBottomIfInLoadedWindow(animated: true)); + await tester.pump(const Duration(milliseconds: 100)); + expect( + coordinator.scrollController.position.isScrollingNotifier.value, + true, + ); + + await pumpCenteredScrollableMessages( + tester, + coordinator, + centerKey: centerKey, + top: top, + bottom: bottom, + keysByMessageId: keysByMessageId, + ); + coordinator.scheduleRestore( + messages: nextMessages, + keysByMessageId: keysByMessageId, + reset: true, + isLatest: false, + hasCenteredAnchor: true, + traceTargetMessageId: bottom.first.messageId, + ); + await tester.pumpAndSettle(); + + expect(coordinator.scrollController.offset, 0); + }); + testWidgets( 'scheduleRestore animates tail-follow without jump notifications', (tester) async {