1
- import 'dart:math' ;
1
+ import 'dart:math' as math ;
2
2
3
3
import 'package:collection/collection.dart' ;
4
4
import 'package:flutter/material.dart' ;
@@ -516,36 +516,47 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
516
516
}
517
517
518
518
final viewportDimension = scrollController.position.viewportDimension;
519
- final maxScrollExtent = scrollController.position.maxScrollExtent;
520
519
final currentScroll = scrollController.position.pixels;
521
520
522
- // If we're within 300px of the bottommost viewport, auto- scroll
523
- if (maxScrollExtent - currentScroll - viewportDimension < 300 ) {
521
+ // If we're one viewportDimension from the bottomList, scroll to it
522
+ if (currentScroll + viewportDimension > 0 ) {
524
523
525
- final distance = scrollController.position.pixels;
526
- final durationMsAtSpeedLimit = (1000 * distance / 8000 ).ceil ();
527
- final durationMs = max (300 , durationMsAtSpeedLimit);
524
+ // Calculate initial scroll parameters
525
+ final distanceToCenter = scrollController.position.pixels;
526
+ final durationMsAtSpeedLimit = (1000 * distanceToCenter / 8000 ).ceil ();
527
+ final durationMs = math.max (300 , durationMsAtSpeedLimit);
528
528
529
- await scrollController.animateTo (
530
- scrollController.position.maxScrollExtent,
531
- duration: Duration (milliseconds: durationMs),
532
- curve: Curves .ease);
529
+ // If we're not at the bottomSliver,scroll to it
530
+ if (distanceToCenter< 36 ){
531
+ await scrollController.animateTo (
532
+ 36 , //Scroll 36 px inside bottomSliver.The sizedBox is 36px high. so theres no chance of overscrolling
533
+ duration: Duration (milliseconds: durationMs),
534
+ curve: Curves .easeIn);
535
+ await Future <void >.delayed (const Duration (milliseconds: 50 ));
536
+ }
533
537
538
+ // Wait for the layout to settle so scrollController.position.pixels is updated properly
534
539
535
540
536
- if (scrollController.position.pixels + 40 < scrollController.position.maxScrollExtent ) {
541
+ final distanceToBottom = scrollController.position.maxScrollExtent - scrollController.position.pixels;
542
+ final durationMsToBottom = math.min (1500 , (1000 * distanceToBottom / 8000 ).ceil ());
543
+ // If we go too fast, we'll overscroll.as
544
+
545
+ // After scroling to the bottom sliver, scroll to the bottom of the bottomSliver if we're not already there
546
+ if (distanceToBottom > 36 ) {
537
547
await scrollController.animateTo (
538
548
scrollController.position.maxScrollExtent,
539
- duration: Duration (milliseconds: durationMs),
540
- curve: Curves .ease);
541
- }
549
+ duration: Duration (milliseconds: durationMsToBottom),
550
+ curve: Curves .ease);
551
+ }
552
+
542
553
}
543
554
});
544
555
}
545
556
}
546
557
547
558
void _handleScrollMetrics (ScrollMetrics scrollMetrics) {
548
- if (scrollMetrics.extentAfter == 0 ) {
559
+ if (scrollMetrics.extentAfter < 40 ) {
549
560
_scrollToBottomVisibleValue.value = false ;
550
561
} else {
551
562
_scrollToBottomVisibleValue.value = true ;
@@ -675,10 +686,13 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
675
686
sliver = SliverSafeArea (sliver: sliver);
676
687
}
677
688
689
+
690
+
678
691
return CustomScrollView (
679
692
// TODO: Offer `ScrollViewKeyboardDismissBehavior.interactive` (or
680
693
// similar) if that is ever offered:
681
694
// https://github.com/flutter/flutter/issues/57609#issuecomment-1355340849
695
+
682
696
keyboardDismissBehavior: switch (Theme .of (context).platform) {
683
697
// This seems to offer the only built-in way to close the keyboard
684
698
// on iOS. It's not ideal; see TODO above.
@@ -734,6 +748,30 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
734
748
}
735
749
}
736
750
751
+
752
+ class NoOverScrollPhysics extends ScrollPhysics {
753
+ const NoOverScrollPhysics ({super .parent});
754
+
755
+ @override
756
+ NoOverScrollPhysics applyTo (ScrollPhysics ? ancestor) {
757
+ return NoOverScrollPhysics (parent: buildParent (ancestor));
758
+ }
759
+
760
+ @override
761
+ double applyBoundaryConditions (ScrollMetrics position, double value) {
762
+ // Prevent overscroll at the top
763
+ if (value < position.minScrollExtent) {
764
+ return position.minScrollExtent;
765
+ }
766
+ // Prevent overscroll at the bottom
767
+ if (value > position.maxScrollExtent) {
768
+ return position.maxScrollExtent;
769
+ }
770
+ return 0.0 ; // Allow normal scrolling within bounds
771
+ }
772
+ }
773
+
774
+
737
775
class ScrollToBottomButton extends StatelessWidget {
738
776
const ScrollToBottomButton ({super .key, required this .scrollController, required this .visibleValue});
739
777
@@ -742,26 +780,34 @@ class ScrollToBottomButton extends StatelessWidget {
742
780
743
781
Future <void > _navigateToBottom () async {
744
782
// Calculate initial scroll parameters
745
- final distance = scrollController.position.pixels;
746
- final durationMsAtSpeedLimit = (1000 * distance / 8000 ).ceil ();
747
- final durationMs = max (300 , durationMsAtSpeedLimit);
783
+ final distanceToCenter = scrollController.position.pixels;
784
+ final durationMsAtSpeedLimit = (1000 * distanceToCenter / 8000 ).ceil ();
785
+ final durationMs = math. max (300 , durationMsAtSpeedLimit);
748
786
749
- // Do a single scroll attempt with a completion check
750
- await scrollController.animateTo (
751
- scrollController.position.maxScrollExtent,
787
+ // If we're not at the bottomSliver,scroll to it
788
+ if (distanceToCenter< 36 ){
789
+ await scrollController.animateTo (
790
+ 36 , //Scroll 36 px inside bottomSliver.The sizedBox is 36px high. so theres no chance of overscrolling
752
791
duration: Duration (milliseconds: durationMs),
753
- curve: Curves .ease);
754
- var count = 1 ;
755
- // Check if we actually reached bottom, if not try again
756
- // This handles cases where content was loaded during scroll
757
- while (scrollController.position.pixels + 40 < scrollController.position.maxScrollExtent) {
792
+ curve: Curves .easeIn);
793
+ }
794
+
795
+
796
+ // Wait for the layout to settle so scrollController.position.pixels is updated properly
797
+ await Future <void >.delayed (const Duration (milliseconds: 50 ));
798
+
799
+
800
+ final distanceToBottom = scrollController.position.maxScrollExtent - scrollController.position.pixels;
801
+ final durationMsToBottom = math.min (1000 , (1000 * distanceToBottom / 8000 ).ceil ());
802
+ // If we go too fast, we'll overscroll.
803
+
804
+ // After scroling to the bottom sliver, scroll to the bottom of the bottomSliver if we're not already there
805
+ if (distanceToBottom > 36 ) {
758
806
await scrollController.animateTo (
759
807
scrollController.position.maxScrollExtent,
760
- duration: const Duration (milliseconds: 300 ),
761
- curve: Curves .ease);
762
- count++ ;
808
+ duration: Duration (milliseconds: durationMsToBottom),
809
+ curve: Curves .easeOut);
763
810
}
764
- print ("count: $count " );
765
811
}
766
812
767
813
@override
@@ -1221,7 +1267,8 @@ class DmRecipientHeader extends StatelessWidget {
1221
1267
.where ((id) => id != store.selfUserId)
1222
1268
.map ((id) => store.users[id]? .fullName ?? zulipLocalizations.unknownUserName)
1223
1269
.sorted ()
1224
- .join (", " ));
1270
+ .join (", " )
1271
+ );
1225
1272
} else {
1226
1273
// TODO pick string; web has glitchy "You and $yourname"
1227
1274
title = zulipLocalizations.messageListGroupYouWithYourself;
0 commit comments