Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<Iterable<ItemPosition>> itemPositionListener;

Expand All @@ -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.
///
Expand All @@ -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<ItemPosition> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class StreamMessageListView extends StatefulWidget {
this.onModeratedMessageTap,
this.onMessageLongPress,
this.showFloatingDateDivider = true,
this.fadeFloatingDateDividerNearInline = true,
this.threadSeparatorBuilder,
this.unreadMessagesSeparatorBuilder,
this.messageListController,
Expand Down Expand Up @@ -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<Message>)? messageListBuilder;

Expand Down Expand Up @@ -903,6 +910,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
child: FloatingDateDivider(
itemCount: itemCount,
reverse: widget.reverse,
fadeNearInlineDivider: widget.fadeFloatingDateDividerNearInline,
itemPositionListener: _itemPositionListener.itemPositions,
messages: messages,
dateDividerBuilder: switch (widget.floatingDateDividerBuilder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ void main() {
FloatingDateDivider(
itemPositionListener: itemPositionListener,
reverse: false,
fadeNearInlineDivider: false,
messages: messages,
itemCount: itemCount,
),
Expand Down Expand Up @@ -381,6 +382,7 @@ void main() {
FloatingDateDivider(
itemPositionListener: itemPositionListener,
reverse: true, // Use getBottomElementIndex
fadeNearInlineDivider: false,
messages: messages,
itemCount: itemCount,
),
Expand Down
Loading