diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart b/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart index 6b9b2b381..908d0d9dd 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -16,9 +18,13 @@ class FloatingDateDivider extends StatelessWidget { required this.reverse, required this.messages, required this.itemCount, + this.fadeNearInlineDivider = true, this.dateDividerBuilder, }); + /// Viewport-fraction over which the floating divider fades out + static const _fadeRange = 0.05; + /// A [ValueListenable] that provides the positions of items in the list view. final ValueListenable> itemPositionListener; @@ -32,6 +38,12 @@ class FloatingDateDivider extends StatelessWidget { /// loaders, headers, and footers. final int itemCount; + /// Whether this divider fades out when an inline date divider for the same + /// date approaches it in the viewport. + /// + /// Defaults to true. + final bool fadeNearInlineDivider; + /// A optional builder function that creates a widget to display the date /// divider. /// @@ -58,18 +70,130 @@ class FloatingDateDivider extends StatelessWidget { // Offset the index to account for two extra items // (loader and footer) at the bottom of the ListView. - final message = messages.elementAtOrNull(index - 2); + final messageIndex = index - 2; + final message = messages.elementAtOrNull(messageIndex); if (message == null) return const Empty(); - if (dateDividerBuilder case final builder?) { - return builder.call(message.createdAt.toLocal()); - } + final divider = switch (dateDividerBuilder) { + final builder? => builder.call(message.createdAt.toLocal()), + _ => StreamDateDivider(dateTime: message.createdAt.toLocal()), + }; + + if (!fadeNearInlineDivider) return divider; - return StreamDateDivider(dateTime: message.createdAt.toLocal()); + final opacity = _floatingDividerOpacity( + positions, + index, + messageIndex, + ); + + if (opacity <= 0) return const Empty(); + if (opacity >= 1) return divider; + + return Opacity(opacity: opacity, child: divider); }, ); } + double _floatingDividerOpacity( + Iterable positions, + int itemIndex, + int messageIndex, + ) { + final messageDate = messages[messageIndex].createdAt.toLocal(); + + final bool hasDateDividerAbove; + final bool hasDateDividerBelow; + + if (reverse) { + hasDateDividerAbove = + messageIndex >= messages.length - 1 || + !_isSameDay( + messageDate, + messages[messageIndex + 1].createdAt.toLocal(), + ); + hasDateDividerBelow = + messageIndex > 0 && + !_isSameDay( + messageDate, + messages[messageIndex - 1].createdAt.toLocal(), + ); + } else { + hasDateDividerAbove = + messageIndex > 0 && + !_isSameDay( + messageDate, + messages[messageIndex - 1].createdAt.toLocal(), + ); + hasDateDividerBelow = + messageIndex < messages.length - 1 && + !_isSameDay( + messageDate, + messages[messageIndex + 1].createdAt.toLocal(), + ); + } + + if (!hasDateDividerAbove && !hasDateDividerBelow) return 1; + + for (final p in positions) { + if (p.index != itemIndex) continue; + + var opacity = 1.0; + + if (reverse) { + // Fade as the inline divider ABOVE becomes visible + // (trailing edge = top of item, 1.0 = viewport top). + if (hasDateDividerAbove && p.itemTrailingEdge < 1) { + opacity = clampDouble( + (p.itemTrailingEdge - (1.0 - _fadeRange)) / _fadeRange, + 0, + 1, + ); + } + + // Fade as the inline divider BELOW approaches the viewport top + // (leading edge = bottom of item, approaching 1.0). + if (hasDateDividerBelow) { + final t = clampDouble( + ((1.0 - _fadeRange) - p.itemLeadingEdge) / _fadeRange, + 0, + 1, + ); + opacity = min(opacity, t); + } + } else { + // Fade as the inline divider ABOVE becomes visible + // (leading edge = top of item, 0.0 = viewport top). + if (hasDateDividerAbove && p.itemLeadingEdge > 0) { + opacity = clampDouble( + (_fadeRange - p.itemLeadingEdge) / _fadeRange, + 0, + 1, + ); + } + + // Fade as the inline divider BELOW approaches the viewport top + // (trailing edge = bottom of item, approaching 0.0). + if (hasDateDividerBelow) { + final t = clampDouble( + (p.itemTrailingEdge - _fadeRange) / _fadeRange, + 0, + 1, + ); + opacity = min(opacity, t); + } + } + + return opacity; + } + + return 1; + } + + static bool _isSameDay(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } + // Returns True if the item index is a valid message index and not one of the // special items (like header, footer, loaders, etc.). bool _isValidMessageIndex(int index) { diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 0a6268c2d..1f18aad9f 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -146,6 +146,7 @@ class StreamMessageListView extends StatefulWidget { this.onModeratedMessageTap, this.onMessageLongPress, this.showFloatingDateDivider = true, + this.fadeFloatingDateDividerNearInline = true, this.threadSeparatorBuilder, this.unreadMessagesSeparatorBuilder, this.messageListController, @@ -354,6 +355,12 @@ class StreamMessageListView extends StatefulWidget { /// Flag for showing the floating date divider final bool showFloatingDateDivider; + /// Whether the floating date divider fades out when an inline date divider + /// for the same date is near the top of the viewport. + /// + /// Only has an effect when [showFloatingDateDivider] is true. + final bool fadeFloatingDateDividerNearInline; + /// Function called when messages are fetched final Widget Function(BuildContext, List)? messageListBuilder; @@ -903,6 +910,7 @@ class _StreamMessageListViewState extends State { child: FloatingDateDivider( itemCount: itemCount, reverse: widget.reverse, + fadeNearInlineDivider: widget.fadeFloatingDateDividerNearInline, itemPositionListener: _itemPositionListener.itemPositions, messages: messages, dateDividerBuilder: switch (widget.floatingDateDividerBuilder) { diff --git a/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart b/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart index 9af5114ba..9535a1964 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart @@ -311,6 +311,7 @@ void main() { FloatingDateDivider( itemPositionListener: itemPositionListener, reverse: false, + fadeNearInlineDivider: false, messages: messages, itemCount: itemCount, ), @@ -381,6 +382,7 @@ void main() { FloatingDateDivider( itemPositionListener: itemPositionListener, reverse: true, // Use getBottomElementIndex + fadeNearInlineDivider: false, messages: messages, itemCount: itemCount, ),