Skip to content

Commit de9283c

Browse files
authored
Fix scroll to index (#304)
* fix: only scroll on first open * fix: scrolling indices cannot be hashCode because they need to be ordered * fix: wrong dates in example messages
1 parent 36e1dd4 commit de9283c

File tree

3 files changed

+63
-23
lines changed

3 files changed

+63
-23
lines changed

example/assets/messages.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"id": "82091008-a484-4a89-ae75-a22bf8d6f3ac",
4848
"lastName": "White"
4949
},
50-
"createdAt": 1655648401000,
50+
"createdAt": 1655648400000,
5151
"id": "64747b28-df19-4a0c-8c47-316dc3546e3c",
5252
"status": "seen",
5353
"text": "Here you go buddy! 💪",
@@ -59,7 +59,7 @@
5959
"id": "b4878b96-efbc-479a-8291-474ef323dec7",
6060
"imageUrl": "https://avatars.githubusercontent.com/u/14123304?v=4"
6161
},
62-
"createdAt": 1655648402000,
62+
"createdAt": 1655648399000,
6363
"id": "4a202811-7d48-4ae9-8323-d764a56031ds",
6464
"status": "seen",
6565
"text": "FIRST UNSEEN\n\nVoluptatem voluptatum eos aut voluptatem occaecati. Quia ducimus vero molestiae molestiae illum illo nisi autem. Labore consectetur expedita illum consequatur inventore consequatur quasi voluptatem. Perspiciatis ut reprehenderit officiis animi voluptas.",
@@ -71,7 +71,7 @@
7171
"id": "82091008-a484-4a89-ae75-a22bf8d6f3ac",
7272
"lastName": "White"
7373
},
74-
"createdAt": 1655648403000,
74+
"createdAt": 1655648398000,
7575
"id": "6a1a4351-cf05-4d0c-9d0f-47ed378b6112",
7676
"mimeType": "application/pdf",
7777
"name": "city_guide-madrid.pdf",
@@ -86,7 +86,7 @@
8686
"id": "4c2307ba-3d40-442f-b1ff-b271f63904ca",
8787
"lastName": "Doe"
8888
},
89-
"createdAt": 1655624464000,
89+
"createdAt": 1655648397000,
9090
"id": "38681a33-2563-42aa-957b-cfc12f791d16",
9191
"status": "seen",
9292
"text": "Matt, where is my Madrid guide?",

lib/src/widgets/chat.dart

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,17 @@ class Chat extends StatefulWidget {
299299

300300
/// [Chat] widget state.
301301
class ChatState extends State<Chat> {
302-
static const int _unseenMessageBannerIndex = 1;
302+
/// Used to get the correct auto scroll index from [_autoScrollIndexByID].
303+
static const String _unseenMessageBannerID = 'unseen_banner_id';
303304
List<Object> _chatMessages = [];
304305
List<PreviewImage> _gallery = [];
305306
PageController? _galleryPageController;
306307
bool _isImageViewVisible = false;
308+
309+
bool _hasScrolledToUnseenOnOpen = false;
310+
311+
/// Keep track of all the auto scroll indices by their respective message's id to allow animating to them.
312+
final Map<String, int> _autoScrollIndexByID = {};
307313
late final AutoScrollController _scrollController;
308314

309315
@override
@@ -336,14 +342,8 @@ class ChatState extends State<Chat> {
336342
_chatMessages = result[0] as List<Object>;
337343
_gallery = result[1] as List<PreviewImage>;
338344

339-
if (widget.scrollToUnseenOptions.scrollOnOpen) {
340-
WidgetsBinding.instance.addPostFrameCallback((_) async {
341-
if (mounted) {
342-
await Future.delayed(widget.scrollToUnseenOptions.scrollDelay);
343-
scrollToFirstUnseen();
344-
}
345-
});
346-
}
345+
_refreshAutoScrollMapping();
346+
_maybeScrollToFirstUnseen();
347347
}
348348
}
349349

@@ -356,14 +356,14 @@ class ChatState extends State<Chat> {
356356

357357
/// Scroll to the unseen message banner.
358358
void scrollToFirstUnseen() => _scrollController.scrollToIndex(
359-
_unseenMessageBannerIndex,
359+
_autoScrollIndexByID[_unseenMessageBannerID]!,
360360
duration: widget.scrollToUnseenOptions.scrollDuration,
361361
);
362362

363363
/// Scroll to the message with the specified [id].
364364
void scrollToMessage(String id, {Duration? duration}) =>
365365
_scrollController.scrollToIndex(
366-
id.hashCode,
366+
_autoScrollIndexByID[id]!,
367367
duration: duration ?? scrollAnimationDuration,
368368
);
369369

@@ -397,8 +397,12 @@ class ChatState extends State<Chat> {
397397
) =>
398398
ChatList(
399399
isLastPage: widget.isLastPage,
400-
itemBuilder: (item, index) =>
401-
_messageBuilder(item, constraints),
400+
itemBuilder: (Object item, int? index) =>
401+
_messageBuilder(
402+
item,
403+
constraints,
404+
index,
405+
),
402406
items: _chatMessages,
403407
keyboardDismissBehavior:
404408
widget.keyboardDismissBehavior,
@@ -448,7 +452,12 @@ class ChatState extends State<Chat> {
448452
),
449453
);
450454

451-
Widget _messageBuilder(Object object, BoxConstraints constraints) {
455+
/// We need the index for auto scrolling because it will scroll until it reaches an index higher or equal that what it is scrolling towards. Index will be null for removed messages. Can just set to -1 for auto scroll.
456+
Widget _messageBuilder(
457+
Object object,
458+
BoxConstraints constraints,
459+
int? index,
460+
) {
452461
if (object is DateHeader) {
453462
if (widget.dateHeaderBuilder != null) {
454463
return widget.dateHeaderBuilder!(object);
@@ -469,7 +478,7 @@ class ChatState extends State<Chat> {
469478
} else if (object is UnseenBanner) {
470479
return AutoScrollTag(
471480
key: const Key('unseen_banner'),
472-
index: _unseenMessageBannerIndex,
481+
index: index ?? -1,
473482
controller: _scrollController,
474483
child: const UnseenMessageBanner(),
475484
);
@@ -483,9 +492,7 @@ class ChatState extends State<Chat> {
483492

484493
return AutoScrollTag(
485494
key: Key('scroll-${message.id}'),
486-
// By using the hashCode as index we can jump to a message using its ID.
487-
// Otherwise, we would have to keep track of a map from ID to index.
488-
index: message.id.hashCode,
495+
index: index ?? -1,
489496
controller: _scrollController,
490497
child: Message(
491498
avatarBuilder: widget.avatarBuilder,
@@ -531,6 +538,37 @@ class ChatState extends State<Chat> {
531538
}
532539
}
533540

541+
/// Updates the [_autoScrollIndexByID] mapping with the latest messages.
542+
void _refreshAutoScrollMapping() {
543+
_autoScrollIndexByID.clear();
544+
var i = 0;
545+
for (final object in _chatMessages) {
546+
if (object is UnseenBanner) {
547+
_autoScrollIndexByID[_unseenMessageBannerID] = i;
548+
} else if (object is Map<String, Object>) {
549+
final message = object['message']! as types.Message;
550+
_autoScrollIndexByID[message.id] = i;
551+
}
552+
i++;
553+
}
554+
}
555+
556+
/// Only scroll to first unseen if there are messages and it is the first open.
557+
void _maybeScrollToFirstUnseen() {
558+
if (widget.scrollToUnseenOptions.scrollOnOpen &&
559+
_chatMessages.isNotEmpty &&
560+
!_hasScrolledToUnseenOnOpen) {
561+
WidgetsBinding.instance.addPostFrameCallback((_) async {
562+
if (mounted) {
563+
await Future.delayed(widget.scrollToUnseenOptions.scrollDelay);
564+
debugPrint('scrolling now');
565+
scrollToFirstUnseen();
566+
}
567+
});
568+
_hasScrolledToUnseenOnOpen = true;
569+
}
570+
}
571+
534572
void _onCloseGalleryPressed() {
535573
setState(() {
536574
_isImageViewVisible = false;

lib/src/widgets/unseen_message_banner.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:flutter/material.dart';
2+
import 'package:scroll_to_index/scroll_to_index.dart'
3+
show scrollAnimationDuration;
24

35
import 'inherited_chat_theme.dart';
46
import 'inherited_l10n.dart';
@@ -30,7 +32,7 @@ class ScrollToUnseenOptions {
3032
const ScrollToUnseenOptions({
3133
this.lastSeenMessageID,
3234
this.scrollDelay = const Duration(milliseconds: 150),
33-
this.scrollDuration = const Duration(milliseconds: 250),
35+
this.scrollDuration = scrollAnimationDuration,
3436
this.scrollOnOpen = false,
3537
});
3638

0 commit comments

Comments
 (0)