@@ -141,12 +141,17 @@ abstract class MessageListPageState {
141
141
}
142
142
143
143
class MessageListPage extends StatefulWidget {
144
- const MessageListPage ({super .key, required this .initNarrow});
144
+ const MessageListPage ({
145
+ super .key,
146
+ required this .initNarrow,
147
+ this .initAnchorMessageId,
148
+ });
145
149
146
150
static AccountRoute <void > buildRoute ({int ? accountId, BuildContext ? context,
147
- required Narrow narrow}) {
151
+ required Narrow narrow, int ? initAnchorMessageId }) {
148
152
return MaterialAccountWidgetRoute (accountId: accountId, context: context,
149
- page: MessageListPage (initNarrow: narrow));
153
+ page: MessageListPage (
154
+ initNarrow: narrow, initAnchorMessageId: initAnchorMessageId));
150
155
}
151
156
152
157
/// The [MessageListPageState] above this context in the tree.
@@ -162,6 +167,7 @@ class MessageListPage extends StatefulWidget {
162
167
}
163
168
164
169
final Narrow initNarrow;
170
+ final int ? initAnchorMessageId; // TODO(#1564) highlight target upon load
165
171
166
172
@override
167
173
State <MessageListPage > createState () => _MessageListPageState ();
@@ -240,6 +246,10 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
240
246
actions.add (_TopicListButton (streamId: streamId));
241
247
}
242
248
249
+ // TODO(#80): default to anchor firstUnread, instead of newest
250
+ final initAnchor = widget.initAnchorMessageId == null
251
+ ? AnchorCode .newest : NumericAnchor (widget.initAnchorMessageId! );
252
+
243
253
// Insert a PageRoot here, to provide a context that can be used for
244
254
// MessageListPage.ancestorOf.
245
255
return PageRoot (child: Scaffold (
@@ -259,7 +269,8 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
259
269
// we matched to the Figma in 21dbae120. See another frame, which uses that:
260
270
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=147%3A9088&mode=dev
261
271
body: Builder (
262
- builder: (BuildContext context) => Column (
272
+ builder: (BuildContext context) {
273
+ return Column (
263
274
// Children are expected to take the full horizontal space
264
275
// and handle the horizontal device insets.
265
276
// The bottom inset should be handled by the last child only.
@@ -279,11 +290,13 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
279
290
child: MessageList (
280
291
key: _messageListKey,
281
292
narrow: narrow,
293
+ initAnchor: initAnchor,
282
294
onNarrowChanged: _narrowChanged,
283
295
))),
284
296
if (ComposeBox .hasComposeBox (narrow))
285
297
ComposeBox (key: _composeBoxKey, narrow: narrow)
286
- ]))));
298
+ ]);
299
+ })));
287
300
}
288
301
}
289
302
@@ -479,9 +492,15 @@ const kFetchMessagesBufferPixels = (kMessageListFetchBatchSize / 2) * _kShortMes
479
492
/// When there is no [ComposeBox] , also takes responsibility
480
493
/// for dealing with the bottom inset.
481
494
class MessageList extends StatefulWidget {
482
- const MessageList ({super .key, required this .narrow, required this .onNarrowChanged});
495
+ const MessageList ({
496
+ super .key,
497
+ required this .narrow,
498
+ required this .initAnchor,
499
+ required this .onNarrowChanged,
500
+ });
483
501
484
502
final Narrow narrow;
503
+ final Anchor initAnchor;
485
504
final void Function (Narrow newNarrow) onNarrowChanged;
486
505
487
506
@override
@@ -504,8 +523,9 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
504
523
505
524
@override
506
525
void onNewStore () { // TODO(#464) try to keep using old model until new one gets messages
526
+ final anchor = _model == null ? widget.initAnchor : _model! .anchor;
507
527
_model? .dispose ();
508
- _initModel (PerAccountStoreWidget .of (context));
528
+ _initModel (PerAccountStoreWidget .of (context), anchor );
509
529
}
510
530
511
531
@override
@@ -516,10 +536,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
516
536
super .dispose ();
517
537
}
518
538
519
- void _initModel (PerAccountStore store) {
520
- // TODO(#82): get anchor as page/route argument, instead of using newest
521
- // TODO(#80): default to anchor firstUnread, instead of newest
522
- final anchor = AnchorCode .newest;
539
+ void _initModel (PerAccountStore store, Anchor anchor) {
523
540
_model = MessageListView .init (store: store,
524
541
narrow: widget.narrow, anchor: anchor);
525
542
model.addListener (_modelChanged);
@@ -535,6 +552,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
535
552
// redirected us to the new location of the operand message ID.
536
553
widget.onNarrowChanged (model.narrow);
537
554
}
555
+ // TODO when model reset, reset scroll
538
556
setState (() {
539
557
// The actual state lives in the [MessageListView] model.
540
558
// This method was called because that just changed.
@@ -557,6 +575,9 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
557
575
// still not yet updated to account for the newly-added messages.
558
576
model.fetchOlder ();
559
577
}
578
+ if (scrollMetrics.extentAfter < kFetchMessagesBufferPixels) {
579
+ model.fetchNewer ();
580
+ }
560
581
}
561
582
562
583
void _scrollChanged () {
@@ -607,6 +628,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
607
628
// MessageList's dartdoc.
608
629
child: SafeArea (
609
630
child: ScrollToBottomButton (
631
+ model: model,
610
632
scrollController: scrollController,
611
633
visible: _scrollToBottomVisible))),
612
634
])))));
@@ -742,13 +764,21 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
742
764
}
743
765
744
766
Widget _buildEndCap () {
745
- return Column (crossAxisAlignment: CrossAxisAlignment .stretch, children: [
746
- TypingStatusWidget (narrow: widget.narrow),
747
- MarkAsReadWidget (narrow: widget.narrow),
748
- // To reinforce that the end of the feed has been reached:
749
- // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Mark-as-read/near/1680603
750
- const SizedBox (height: 36 ),
751
- ]);
767
+ if (model.haveNewest) {
768
+ return Column (crossAxisAlignment: CrossAxisAlignment .stretch, children: [
769
+ TypingStatusWidget (narrow: widget.narrow),
770
+ // TODO perhaps offer mark-as-read even when not done fetching?
771
+ MarkAsReadWidget (narrow: widget.narrow),
772
+ // To reinforce that the end of the feed has been reached:
773
+ // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Mark-as-read/near/1680603
774
+ const SizedBox (height: 36 ),
775
+ ]);
776
+ } else if (model.busyFetchingMore) {
777
+ // See [_buildStartCap] for why this condition shows a loading indicator.
778
+ return const _MessageListLoadingMore ();
779
+ } else {
780
+ return SizedBox .shrink ();
781
+ }
752
782
}
753
783
754
784
Widget _buildItem (MessageListItem data) {
@@ -798,13 +828,40 @@ class _MessageListLoadingMore extends StatelessWidget {
798
828
}
799
829
800
830
class ScrollToBottomButton extends StatelessWidget {
801
- const ScrollToBottomButton ({super .key, required this .scrollController, required this .visible});
831
+ const ScrollToBottomButton ({
832
+ super .key,
833
+ required this .model,
834
+ required this .scrollController,
835
+ required this .visible,
836
+ });
802
837
803
- final ValueNotifier < bool > visible ;
838
+ final MessageListView model ;
804
839
final MessageListScrollController scrollController;
840
+ final ValueNotifier <bool > visible;
805
841
806
842
void _scrollToBottom () {
807
- scrollController.position.scrollToEnd ();
843
+ if (model.haveNewest) {
844
+ // Scrolling smoothly from here to the bottom won't require any requests
845
+ // to the server.
846
+ // It also probably isn't *that* far away: the user must have scrolled
847
+ // here from there (or from near enough that a fetch reached there),
848
+ // so scrolling back there -- at top speed -- shouldn't take too long.
849
+ // Go for it.
850
+ scrollController.position.scrollToEnd ();
851
+ } else {
852
+ // This message list doesn't have the messages for the bottom of history.
853
+ // There could be quite a lot of history between here and there --
854
+ // for example, at first unread in the combined feed or a busy channel,
855
+ // for a user who has some old unreads going back months and years.
856
+ // In that case trying to scroll smoothly to the bottom is hopeless.
857
+ //
858
+ // Given that there were at least 100 messages between this message list's
859
+ // initial anchor and the end of history (or else `fetchInitial` would
860
+ // have reached the end at the outset), that situation is very likely.
861
+ // Even if the end is close by, it's at least one fetch away.
862
+ // Instead of scrolling, jump to the end, which is always just one fetch.
863
+ model.jumpToEnd ();
864
+ }
808
865
}
809
866
810
867
@override
0 commit comments