Skip to content

Commit 122a516

Browse files
authored
fix(ui): improve scroll to bottom and floating date divider (#2289)
1 parent b4110ff commit 122a516

File tree

5 files changed

+454
-61
lines changed

5 files changed

+454
-61
lines changed

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## Upcoming
2+
3+
🐞 Fixed
4+
5+
- Fixed `FloatingDateDivider` not showing the correct date when the latest message was too big and
6+
exceeded the viewport main axis size.
7+
- Fixed `ScrollToBottom` button always showing when the latest message was too big and exceeded the
8+
viewport main axis size.
9+
110
## 9.12.0
211

312
✅ Added

packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,33 @@ class FloatingDateDivider extends StatelessWidget {
1616
required this.reverse,
1717
required this.messages,
1818
required this.itemCount,
19+
@Deprecated('No longer used, Will be removed in future versions.')
1920
this.isThreadConversation = false,
2021
this.dateDividerBuilder,
2122
});
2223

2324
/// true if this is a thread conversation
25+
@Deprecated('No longer used, Will be removed in future versions.')
2426
final bool isThreadConversation;
2527

26-
// ignore: public_member_api_docs
28+
/// A [ValueListenable] that provides the positions of items in the list view.
2729
final ValueListenable<Iterable<ItemPosition>> itemPositionListener;
2830

29-
// ignore: public_member_api_docs
31+
/// Whether the list is reversed or not.
3032
final bool reverse;
3133

32-
// ignore: public_member_api_docs
34+
/// The list of messages which are displayed in the list view.
3335
final List<Message> messages;
3436

35-
// ignore: public_member_api_docs
37+
/// The total number of items in the list view, including special items like
38+
/// loaders, headers, and footers.
3639
final int itemCount;
3740

38-
// ignore: public_member_api_docs
41+
/// A optional builder function that creates a widget to display the date
42+
/// divider.
43+
///
44+
/// If provided, this function will be called with the date of the message
45+
/// to create the date divider widget.
3946
final Widget Function(DateTime)? dateDividerBuilder;
4047

4148
@override
@@ -47,29 +54,37 @@ class FloatingDateDivider extends StatelessWidget {
4754
return const Empty();
4855
}
4956

50-
var index = switch (reverse) {
57+
final index = switch (reverse) {
5158
true => getBottomElementIndex(positions),
5259
false => getTopElementIndex(positions),
5360
};
5461

55-
if ((index == null) ||
56-
(!isThreadConversation && index == itemCount - 2) ||
57-
(isThreadConversation && index == itemCount - 1)) {
58-
return const Empty();
59-
}
62+
if (index == null) return const Empty();
63+
if (!_isValidMessageIndex(index)) return const Empty();
64+
65+
// Offset the index to account for two extra items
66+
// (loader and footer) at the bottom of the ListView.
67+
final message = messages.elementAtOrNull(index - 2);
68+
if (message == null) return const Empty();
6069

61-
if (index <= 2 || index >= itemCount - 3) {
62-
if (reverse) {
63-
index = itemCount - 4;
64-
} else {
65-
index = 2;
66-
}
70+
if (dateDividerBuilder case final builder?) {
71+
return builder.call(message.createdAt.toLocal());
6772
}
6873

69-
final message = messages[index - 2];
70-
return dateDividerBuilder?.call(message.createdAt.toLocal()) ??
71-
StreamDateDivider(dateTime: message.createdAt.toLocal());
74+
return StreamDateDivider(dateTime: message.createdAt.toLocal());
7275
},
7376
);
7477
}
78+
79+
// Returns True if the item index is a valid message index and not one of the
80+
// special items (like header, footer, loaders, etc.).
81+
bool _isValidMessageIndex(int index) {
82+
if (index == itemCount - 1) return false; // Parent Message
83+
if (index == itemCount - 2) return false; // Header Builder
84+
if (index == itemCount - 3) return false; // Top Loader Builder
85+
if (index == 1) return false; // Bottom Loader Builder
86+
if (index == 0) return false; // Footer Builder
87+
88+
return true;
89+
}
7590
}

packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,6 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
857857
itemPositionListener: _itemPositionListener.itemPositions,
858858
messages: messages,
859859
dateDividerBuilder: widget.dateDividerBuilder,
860-
isThreadConversation: _isThreadConversation,
861860
),
862861
),
863862
if (widget.showScrollToBottom)
@@ -1452,14 +1451,20 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
14521451
final itemPositions = _itemPositionListener.itemPositions.value.toList();
14531452
if (itemPositions.isEmpty) return;
14541453

1455-
final isLastItemFullyVisible = isElementAtIndexVisible(
1456-
itemPositions,
1457-
fullyVisible: true,
1458-
// Index of the last item in the list view is 2 as 1 is the progress
1459-
// indicator and 0 is the footer.
1460-
index: 2,
1454+
// Index of the last item in the list view is 2 as 1 is the progress
1455+
// indicator and 0 is the footer.
1456+
const lastItemIndex = 2;
1457+
final lastItemPosition = itemPositions.firstWhereOrNull(
1458+
(position) => position.index == lastItemIndex,
14611459
);
14621460

1461+
var isLastItemFullyVisible = false;
1462+
if (lastItemPosition != null) {
1463+
// We consider the last item fully visible if its leading edge (reversed)
1464+
// is greater than or equal to 0.
1465+
isLastItemFullyVisible = lastItemPosition.itemLeadingEdge >= 0;
1466+
}
1467+
14631468
if (mounted) _showScrollToBottom.value = !isLastItemFullyVisible;
14641469
if (isLastItemFullyVisible) return _handleLastItemFullyVisible();
14651470
}

packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,35 @@ int getInitialIndex(
4343

4444
/// Gets the index of the top element in the viewport.
4545
int? getTopElementIndex(Iterable<ItemPosition> values) {
46-
final inView = values.where((position) => position.itemTrailingEdge > 0);
47-
if (inView.isEmpty) return null;
46+
final inView = values.where((position) {
47+
if (position.itemLeadingEdge == position.itemTrailingEdge) {
48+
// If the item's leading and trailing edges are the same, it means the
49+
// item isn't actually rendering anything in the viewport.
50+
return false;
51+
}
52+
53+
return position.itemTrailingEdge > 0;
54+
});
4855

56+
if (inView.isEmpty) return null;
4957
return inView.reduce((min, position) {
5058
return position.itemTrailingEdge < min.itemTrailingEdge ? position : min;
5159
}).index;
5260
}
5361

5462
/// Gets the index of the bottom element in the viewport.
5563
int? getBottomElementIndex(Iterable<ItemPosition> values) {
56-
final inView = values.where((position) => position.itemLeadingEdge < 1);
57-
if (inView.isEmpty) return null;
64+
final inView = values.where((position) {
65+
if (position.itemLeadingEdge == position.itemTrailingEdge) {
66+
// If the item's leading and trailing edges are the same, it means the
67+
// item isn't actually rendering anything in the viewport.
68+
return false;
69+
}
70+
71+
return position.itemLeadingEdge < 1;
72+
});
5873

74+
if (inView.isEmpty) return null;
5975
return inView.reduce((max, position) {
6076
return position.itemLeadingEdge > max.itemLeadingEdge ? position : max;
6177
}).index;

0 commit comments

Comments
 (0)