Skip to content

Commit 6512c1e

Browse files
Finished the job but idk why the tests are failing.Have to troubleshoot
1 parent 3b09c73 commit 6512c1e

File tree

2 files changed

+133
-55
lines changed

2 files changed

+133
-55
lines changed

lib/model/message_list.dart

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ mixin _MessageSequence {
167167
/// before, between, or after the messages.
168168
///
169169
/// This information is completely derived from [messages] and
170-
/// the flags [haveOldest], [fetchingOlder] and [fetchOlderCoolingDown].
170+
/// the flags [haveOldest], [fetchingOlder] and [fetchOlderCoolingDown]
171+
/// and [haveNewest], [fetchingNewer] and [fetchNewerCoolingDown].
171172
/// It exists as an optimization, to memoize that computation.
172173
final QueueList<MessageListItem> items = QueueList();
173174

@@ -476,16 +477,20 @@ bool _sameDay(DateTime date1, DateTime date2) {
476477
/// * Add listeners with [addListener].
477478
/// * Fetch messages with [fetchInitial]. When the fetch completes, this object
478479
/// will notify its listeners (as it will any other time the data changes.)
479-
/// * Fetch more messages as needed with [fetchOlder].
480+
/// * Fetch more messages as needed with [fetchOlder] or [fetchNewer].
480481
/// * On reassemble, call [reassemble].
481482
/// * When the object will no longer be used, call [dispose] to free
482483
/// resources on the [PerAccountStore].
483484
class MessageListView with ChangeNotifier, _MessageSequence {
484-
MessageListView._({required this.store, required this.narrow});
485+
MessageListView._({required this.store, required this.narrow, this.anchorMessageId});
485486

487+
// Anchor message ID is used to fetch messages from a specific point in the list.
488+
// It is set when the user navigates to a message list page with a specific anchor message.
489+
int? anchorMessageId;
490+
int? get anchorIndex => anchorMessageId != null ? findItemWithMessageId(anchorMessageId!) : null;
486491
factory MessageListView.init(
487-
{required PerAccountStore store, required Narrow narrow}) {
488-
final view = MessageListView._(store: store, narrow: narrow);
492+
{required PerAccountStore store, required Narrow narrow, int? anchorMessageId}) {
493+
final view = MessageListView._(store: store, narrow: narrow, anchorMessageId: anchorMessageId);
489494
store.registerMessageList(view);
490495
return view;
491496
}
@@ -564,15 +569,13 @@ class MessageListView with ChangeNotifier, _MessageSequence {
564569
}
565570
}
566571

567-
/// The message ID to use as an anchor when fetching messages.
568-
/// If null, the latest messages will be fetched.
569-
int? anchorMessageId;
572+
570573

571574
/// Fetch messages, starting from scratch.
572575
Future<void> fetchInitial() async {
573576
// TODO(#80): fetch from anchor firstUnread, instead of newest
574577

575-
assert(!fetched && !haveOldest && !fetchingOlder && !fetchOlderCoolingDown);
578+
assert(!fetched && !haveOldest && !fetchingOlder && !fetchOlderCoolingDown && !fetchingNewer && !fetchNewerCoolingDown && !haveNewest);
576579
assert(messages.isEmpty && contents.isEmpty);
577580
// TODO schedule all this in another isolate
578581
final generation = this.generation;
@@ -585,9 +588,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
585588
? kMessageListFetchBatchSize ~/ 2 // Fetch messages before and after anchor
586589
: kMessageListFetchBatchSize, // Fetch only older messages when no anchor
587590
numAfter: anchorMessageId != null
588-
? kMessageListFetchBatchSize ~/ 2 // Fetch messages before and after anchor
589-
: 0, // Don't fetch newer messages when no anchor
591+
? kMessageListFetchBatchSize ~/2 // Fetch messages before and after anchor
592+
: 0, // Don't fetch newer messages when no anchor
590593
);
594+
anchorMessageId ??= result.messages.last.id;
595+
591596
if (this.generation > generation) return;
592597
store.reconcileMessages(result.messages);
593598
store.recentSenders.handleMessages(result.messages); // TODO(#824)
@@ -598,16 +603,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
598603
}
599604
_fetched = true;
600605
_haveOldest = result.foundOldest;
606+
_haveNewest = result.foundNewest;
601607
_updateEndMarkers();
602608
notifyListeners();
603609
}
604610

605-
/// Initialize the view with a specific anchor message.
606-
void setAnchorMessage(int messageId) {
607-
anchorMessageId = messageId;
608-
_reset();
609-
fetchInitial();
610-
}
611611

612612
/// Fetch the next batch of older messages, if applicable.
613613
Future<void> fetchOlder() async {
@@ -720,8 +720,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
720720

721721
_insertAllMessages(messages.length, fetchedMessages);
722722

723-
// No foundNewest in API, assume we're caught up if we get fewer messages
724-
_haveNewest = result.messages.length < kMessageListFetchBatchSize;
723+
_haveNewest = result.foundNewest;
725724

726725
} finally {
727726
if (this.generation == generation) {

lib/widgets/message_list.dart

Lines changed: 115 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,12 @@ abstract class MessageListPageState {
182182
}
183183

184184
class 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;
443443
const kFetchMessagesBufferPixels = (kMessageListFetchBatchSize / 2) * _kShortMessageHeight;
444444

445445
class 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

Comments
 (0)