@@ -182,12 +182,12 @@ abstract class MessageListPageState {
182182}
183183
184184class MessageListPage extends StatefulWidget {
185- const MessageListPage ({super .key, required this .initNarrow});
186-
185+ const MessageListPage ({super .key, required this .initNarrow, this .anchorMessageId });
186+ final int ? anchorMessageId;
187187 static Route <void > buildRoute ({int ? accountId, BuildContext ? context,
188- required Narrow narrow}) {
188+ required Narrow narrow, int ? anchorMessageId }) {
189189 return MaterialAccountWidgetRoute (accountId: accountId, context: context,
190- page: MessageListPage (initNarrow: narrow));
190+ page: MessageListPage (initNarrow: narrow, anchorMessageId : anchorMessageId ));
191191 }
192192
193193 /// The [MessageListPageState] above this context in the tree.
@@ -302,7 +302,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
302302 removeBottom: ComposeBox .hasComposeBox (narrow),
303303
304304 child: Expanded (
305- child: MessageList (narrow: narrow, onNarrowChanged: _narrowChanged))),
305+ child: MessageList (narrow: narrow, onNarrowChanged: _narrowChanged, anchorMessageId : widget.anchorMessageId ))),
306306 if (ComposeBox .hasComposeBox (narrow))
307307 ComposeBox (key: _composeBoxKey, narrow: narrow)
308308 ]))));
@@ -443,11 +443,11 @@ const _kShortMessageHeight = 80;
443443const kFetchMessagesBufferPixels = (kMessageListFetchBatchSize / 2 ) * _kShortMessageHeight;
444444
445445class MessageList extends StatefulWidget {
446- const MessageList ({super .key, required this .narrow, required this .onNarrowChanged});
446+ const MessageList ({super .key, required this .narrow, required this .onNarrowChanged, this .anchorMessageId });
447447
448448 final Narrow narrow;
449449 final void Function (Narrow newNarrow) onNarrowChanged;
450-
450+ final int ? anchorMessageId;
451451 @override
452452 State <StatefulWidget > createState () => _MessageListState ();
453453}
@@ -456,6 +456,8 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
456456 MessageListView ? model;
457457 final ScrollController scrollController = ScrollController ();
458458 final ValueNotifier <bool > _scrollToBottomVisibleValue = ValueNotifier <bool >(false );
459+ List <MessageListItem > newItems = [];
460+ List <MessageListItem > oldItems = [];
459461
460462 @override
461463 void initState () {
@@ -476,10 +478,14 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
476478 super .dispose ();
477479 }
478480
479- void _initModel (PerAccountStore store) {
480- model = MessageListView .init (store: store, narrow: widget.narrow);
481+ void _initModel (PerAccountStore store) async {
482+ model = MessageListView .init (store: store, narrow: widget.narrow, anchorMessageId : widget.anchorMessageId );
481483 model! .addListener (_modelChanged);
482- model! .fetchInitial ();
484+ await model! .fetchInitial ();
485+ setState (() {
486+ oldItems = model! .items.sublist (0 , model! .anchorIndex! + 1 );
487+ newItems = model! .items.sublist (model! .anchorIndex! + 1 , model! .items.length);
488+ });
483489 }
484490
485491 void _modelChanged () {
@@ -488,10 +494,54 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
488494 // [PropagateMode.changeAll] or [PropagateMode.changeLater].
489495 widget.onNarrowChanged (model! .narrow);
490496 }
497+
498+ final previousLength = oldItems.length + newItems.length;
499+
491500 setState (() {
501+ oldItems = model! .items.sublist (0 , model! .anchorIndex! + 1 );
502+ newItems = model! .items.sublist (model! .anchorIndex! + 1 , model! .items.length);
492503 // The actual state lives in the [MessageListView] model.
493504 // This method was called because that just changed.
494505 });
506+
507+
508+ // Auto-scroll when new messages arrive if we're already near the bottom
509+ if (model! .items.length > previousLength && // New messages were added
510+ scrollController.hasClients) {
511+ // Use post-frame callback to ensure scroll metrics are up to date
512+ WidgetsBinding .instance.addPostFrameCallback ((_) async {
513+ // This is to prevent auto-scrolling when fetching newer messages
514+ if (model! .fetchingNewer || model! .fetchingOlder || model! .fetchNewerCoolingDown || model! .fetchOlderCoolingDown || ! model! .haveNewest ){
515+ return ;
516+ }
517+
518+ final viewportDimension = scrollController.position.viewportDimension;
519+ final maxScrollExtent = scrollController.position.maxScrollExtent;
520+ final currentScroll = scrollController.position.pixels;
521+
522+ // If we're within 300px of the bottommost viewport, auto-scroll
523+ if (maxScrollExtent - currentScroll - viewportDimension < 300 ) {
524+
525+ final distance = scrollController.position.pixels;
526+ final durationMsAtSpeedLimit = (1000 * distance / 8000 ).ceil ();
527+ final durationMs = max (300 , durationMsAtSpeedLimit);
528+
529+ await scrollController.animateTo (
530+ scrollController.position.maxScrollExtent,
531+ duration: Duration (milliseconds: durationMs),
532+ curve: Curves .ease);
533+
534+
535+
536+ if (scrollController.position.pixels + 40 < scrollController.position.maxScrollExtent ) {
537+ await scrollController.animateTo (
538+ scrollController.position.maxScrollExtent,
539+ duration: Duration (milliseconds: durationMs),
540+ curve: Curves .ease);
541+ }
542+ }
543+ });
544+ }
495545 }
496546
497547 void _handleScrollMetrics (ScrollMetrics scrollMetrics) {
@@ -510,6 +560,11 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
510560 // still not yet updated to account for the newly-added messages.
511561 model? .fetchOlder ();
512562 }
563+
564+ // Check for fetching newer messages when near the bottom
565+ if (scrollMetrics.extentAfter < kFetchMessagesBufferPixels) {
566+ model? .fetchNewer ();
567+ }
513568 }
514569
515570 void _scrollChanged () {
@@ -562,7 +617,8 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
562617 }
563618
564619 Widget _buildListView (BuildContext context) {
565- final length = model! .items.length;
620+ final length = oldItems.length;
621+ final newLength = newItems.length;
566622 const centerSliverKey = ValueKey ('center sliver' );
567623
568624 Widget sliver = SliverStickyHeaderList (
@@ -587,22 +643,32 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
587643 final valueKey = key as ValueKey <int >;
588644 final index = model! .findItemWithMessageId (valueKey.value);
589645 if (index == - 1 ) return null ;
590- return length - 1 - ( index - 3 ) ;
646+ return length - 1 - index;
591647 },
592- childCount: length + 3 ,
648+ childCount: length,
593649 (context, i) {
594- // To reinforce that the end of the feed has been reached:
595- // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Mark-as-read/near/1680603
596- if (i == 0 ) return const SizedBox (height: 36 );
597-
598- if (i == 1 ) return MarkAsReadWidget (narrow: widget.narrow);
599-
600- if (i == 2 ) return TypingStatusWidget (narrow: widget.narrow);
601-
602- final data = model! .items[length - 1 - (i - 3 )];
650+ final data = oldItems[length - 1 - i];
603651 return _buildItem (data, i);
604652 }));
605653
654+ Widget newMessagesSliver = SliverStickyHeaderList (
655+ headerPlacement: HeaderPlacement .scrollingStart,
656+ delegate: SliverChildBuilderDelegate (
657+ findChildIndexCallback: (Key key) {
658+ final valueKey = key as ValueKey <int >;
659+ final index = model! .findItemWithMessageId (valueKey.value);
660+ if (index == - 1 ) return null ;
661+ return index- 3 ;
662+ },
663+ childCount: newLength+ 3 ,
664+ (context, i) {
665+ if (i == newLength) return TypingStatusWidget (narrow: widget.narrow);
666+ if (i == newLength+ 1 ) return MarkAsReadWidget (narrow: widget.narrow);
667+ if (i == newLength+ 2 ) return const SizedBox (height: 36 );
668+ final data = newItems[i];
669+ return _buildItem (data, i- newLength);
670+ }));
671+
606672 if (! ComposeBox .hasComposeBox (widget.narrow)) {
607673 // TODO(#311) If we have a bottom nav, it will pad the bottom
608674 // inset, and this shouldn't be necessary
@@ -623,16 +689,16 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
623689
624690 controller: scrollController,
625691 semanticChildCount: length + 2 ,
626- anchor: 1.0 ,
692+ anchor: 0.85 ,
627693 center: centerSliverKey,
628694
629695 slivers: [
630- sliver,
631-
632- // This is a trivial placeholder that occupies no space. Its purpose is
633- // to have the key that's passed to [ScrollView.center], and so to cause
634- // the above [SliverStickyHeaderList] to run from bottom to top.
696+ sliver, // Main message list (grows upward)
697+ // Center point - everything before this grows up, everything after grows down
635698 const SliverToBoxAdapter (key: centerSliverKey),
699+ // Static widgets and new messages (will grow downward)
700+ newMessagesSliver, // New messages list (will grow downward)
701+
636702 ]);
637703 }
638704
@@ -674,14 +740,28 @@ class ScrollToBottomButton extends StatelessWidget {
674740 final ValueNotifier <bool > visibleValue;
675741 final ScrollController scrollController;
676742
677- Future <void > _navigateToBottom () {
743+ Future <void > _navigateToBottom () async {
744+ // Calculate initial scroll parameters
678745 final distance = scrollController.position.pixels;
679746 final durationMsAtSpeedLimit = (1000 * distance / 8000 ).ceil ();
680747 final durationMs = max (300 , durationMsAtSpeedLimit);
681- return scrollController.animateTo (
682- 0 ,
748+
749+ // Do a single scroll attempt with a completion check
750+ await scrollController.animateTo (
751+ scrollController.position.maxScrollExtent,
683752 duration: Duration (milliseconds: durationMs),
684753 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) {
758+ await scrollController.animateTo (
759+ scrollController.position.maxScrollExtent,
760+ duration: const Duration (milliseconds: 300 ),
761+ curve: Curves .ease);
762+ count++ ;
763+ }
764+ print ("count: $count " );
685765 }
686766
687767 @override
@@ -728,6 +808,7 @@ class _TypingStatusWidgetState extends State<TypingStatusWidget> with PerAccount
728808 }
729809
730810 void _modelChanged () {
811+
731812 setState (() {
732813 // The actual state lives in [model].
733814 // This method was called because that just changed.
@@ -1358,11 +1439,9 @@ class MessageWithPossibleSender extends StatelessWidget {
13581439 selfUserId: store.selfUserId),
13591440 };
13601441 Navigator .push (context,
1361- MessageListPage .buildRoute (context: context, narrow: narrow));
1362- final messageListState = context.findAncestorStateOfType <_MessageListState >();
1363- if (messageListState != null ) {
1364- messageListState.model? .setAnchorMessage (message.id);
1365- }
1442+ MessageListPage .buildRoute (context: context, narrow: narrow, anchorMessageId: message.id));
1443+
1444+
13661445 }
13671446 },
13681447 child: Padding (
0 commit comments