From db801f4cdbc9626a3128c518b0e1e04860d161d3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Jun 2021 17:18:48 +0530 Subject: [PATCH 1/5] add support for messageListView header/footer Signed-off-by: Sahil Kumar --- .../lib/src/message_list_view.dart | 110 +++++++++++++----- 1 file changed, 80 insertions(+), 30 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index aae9d193..c7966c3d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1,3 +1,4 @@ +// ignore_for_file: lines_longer_than_80_chars import 'dart:async'; import 'dart:math'; @@ -147,6 +148,8 @@ class MessageListView extends StatefulWidget { this.messageHighlightColor, this.onShowMessage, this.showConnectionStateTile = false, + this.headerBuilder, + this.footerBuilder, this.loadingBuilder, this.emptyBuilder, this.systemMessageBuilder, @@ -235,6 +238,12 @@ class MessageListView extends StatefulWidget { /// Function called when messages are fetched final Widget Function(BuildContext, List)? messageListBuilder; + /// + final WidgetBuilder? headerBuilder; + + /// + final WidgetBuilder? footerBuilder; + /// Function used to build a loading widget final WidgetBuilder? loadingBuilder; @@ -407,6 +416,12 @@ class _MessageListViewState extends State { break; } + final itemCount = messages.length + // total messages + 2 + // top + bottom loading indicator + 2 + // header + footer + 1 // parent message + ; + return InfoTile( showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, @@ -448,16 +463,49 @@ class _MessageListViewState extends State { itemScrollController: _scrollController, reverse: true, addAutomaticKeepAlives: false, - itemCount: - messages.length + 2 + (_isThreadConversation ? 1 : 0), + itemCount: itemCount, + + // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) + // eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)| + // ParentMessage -> 7 (count-1) + // Separator(ThreadSeparator) -> 6 (count-2) + // Header -> 6 (count-2) + // Separator(Header -> 8??52) -> 5 (count-3) + // TopLoader -> 5 (count-3) + // Separator(2||8) -> 4 (count-4) + // Message -> 4 (count-4) + // Separator(2||8) -> 3 (count-5) + // Message -> 3 (count-5) + // Separator(2||8) -> 2 (count-6) + // Message -> 2 (count-6) + // Separator(2||8) -> 1 (count-7) + // BottomLoader -> 1 (count-7) + // Separator(Footer -> 8??30) -> 0 (count-8) + // Footer -> 0 (count-8) + separatorBuilder: (context, i) { - if (i == messages.length) return const Offstage(); - if (i == 0) return const SizedBox(height: 30); - if (i == messages.length + 1) { + if (i == itemCount - 2) { + if (widget.parentMessage == null) { + return const Offstage(); + } return _buildThreadSeparator(); } + if (i == itemCount - 3) { + if (widget.headerBuilder == null) { + return const SizedBox(height: 52); + } + return const SizedBox(height: 8); + } + if (i == 0) { + if (widget.footerBuilder == null) { + return const SizedBox(height: 30); + } + return const SizedBox(height: 8); + } - final message = messages[i]; + if (i == 1 || i == itemCount - 4) return const Offstage(); + + final message = messages[i - 2]; final nextMessage = messages[i - 1]; if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), @@ -494,40 +542,51 @@ class _MessageListViewState extends State { return const SizedBox(height: 2); }, itemBuilder: (context, i) { - if (i == messages.length + 2) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder!( - context, - widget.parentMessage, - ); - } else { - return buildParentMessage(widget.parentMessage!); - } + if (i == itemCount - 1) { + if (widget.parentMessage == null) return const Offstage(); + return widget.parentMessageBuilder?.call( + context, + widget.parentMessage, + ) ?? + buildParentMessage(widget.parentMessage!); } - if (i == messages.length + 1) { + + if (i == itemCount - 2) { + return widget.headerBuilder?.call(context) ?? + const Offstage(); + } + + if (i == itemCount - 3) { return _buildLoadingIndicator( streamChannel!, QueryDirection.top, ); } - if (i == 0) { + + if (i == 1) { return _buildLoadingIndicator( streamChannel!, QueryDirection.bottom, ); } - final message = messages[i - 1]; + + if (i == 0) { + return widget.footerBuilder?.call(context) ?? + const Offstage(); + } + + final message = messages[i - 2]; Widget messageWidget; - if (i == 1) { + if (i == 2) { messageWidget = _buildBottomMessage( context, message, messages, streamChannel!, ); - } else if (i == messages.length - 1) { + } else if (i == messages.length - 4) { messageWidget = _buildTopMessage( context, message, @@ -578,7 +637,6 @@ class _MessageListViewState extends State { child: Padding( padding: const EdgeInsets.all(8), child: Text( - // ignore: lines_longer_than_80_chars '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, style: _streamTheme.channelTheme.channelHeaderTheme.subtitle, @@ -1238,15 +1296,7 @@ class _LoadingIndicator extends StatelessWidget { ), ), builder: (context, data) { - if (!data) { - if (!isThreadConversation && direction == QueryDirection.top) { - return const SizedBox( - height: 52, - width: double.infinity, - ); - } - return const Offstage(); - } + if (!data) return const Offstage(); return const Center( child: Padding( padding: EdgeInsets.all(8), From 3b39e84e8fccaa98788b76b4f429a85308939df2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Jun 2021 17:44:07 +0530 Subject: [PATCH 2/5] fix bottomMessageIndex Signed-off-by: Sahil Kumar --- .../lib/src/message_list_view.dart | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index c7966c3d..b5008054 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -575,24 +575,26 @@ class _MessageListViewState extends State { const Offstage(); } - final message = messages[i - 2]; + final topMessageIndex = itemCount - 4; + const bottomMessageIndex = 2; + final message = messages[i - 2]; Widget messageWidget; - if (i == 2) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel!, - ); - } else if (i == messages.length - 4) { + if (i == topMessageIndex) { messageWidget = _buildTopMessage( context, message, messages, streamChannel, ); + } else if (i == bottomMessageIndex) { + messageWidget = _buildBottomMessage( + context, + message, + messages, + streamChannel!, + ); } else { if (widget.messageBuilder != null) { messageWidget = Builder( From 2816124c553e10e5b0e6ada087498ed5a2d357a9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 24 Jun 2021 18:19:52 +0530 Subject: [PATCH 3/5] fix floatingDate, messageWidgetBuilder index Signed-off-by: Sahil Kumar --- .../lib/src/message_list_view.dart | 90 +++++++++---------- .../lib/src/better_stream_builder.dart | 7 +- 2 files changed, 46 insertions(+), 51 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index b5008054..3194827d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -396,6 +396,12 @@ class _MessageListViewState extends State { _messageListLength = newMessagesListLength; + final itemCount = messages.length + // total messages + 2 + // top + bottom loading indicator + 2 + // header + footer + 1 // parent message + ; + return Stack( alignment: Alignment.center, children: [ @@ -416,12 +422,6 @@ class _MessageListViewState extends State { break; } - final itemCount = messages.length + // total messages - 2 + // top + bottom loading indicator - 2 + // header + footer - 1 // parent message - ; - return InfoTile( showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, @@ -470,15 +470,15 @@ class _MessageListViewState extends State { // ParentMessage -> 7 (count-1) // Separator(ThreadSeparator) -> 6 (count-2) // Header -> 6 (count-2) - // Separator(Header -> 8??52) -> 5 (count-3) + // Separator(Header -> 8??T -> 0||52) -> 5 (count-3) // TopLoader -> 5 (count-3) - // Separator(2||8) -> 4 (count-4) + // Separator(0) -> 4 (count-4) // Message -> 4 (count-4) // Separator(2||8) -> 3 (count-5) // Message -> 3 (count-5) // Separator(2||8) -> 2 (count-6) // Message -> 2 (count-6) - // Separator(2||8) -> 1 (count-7) + // Separator(0) -> 1 (count-7) // BottomLoader -> 1 (count-7) // Separator(Footer -> 8??30) -> 0 (count-8) // Footer -> 0 (count-8) @@ -492,6 +492,7 @@ class _MessageListViewState extends State { } if (i == itemCount - 3) { if (widget.headerBuilder == null) { + if (_isThreadConversation) return const Offstage(); return const SizedBox(height: 52); } return const SizedBox(height: 8); @@ -505,8 +506,8 @@ class _MessageListViewState extends State { if (i == 1 || i == itemCount - 4) return const Offstage(); - final message = messages[i - 2]; - final nextMessage = messages[i - 1]; + final message = messages[i - 1]; + final nextMessage = messages[i - 2]; if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), Units.DAY, @@ -575,8 +576,9 @@ class _MessageListViewState extends State { const Offstage(); } - final topMessageIndex = itemCount - 4; - const bottomMessageIndex = 2; + final topMessageIndex = itemCount - + 4; // 7 -> parent // 6 -> header // 5 -> loader + const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer final message = messages[i - 2]; Widget messageWidget; @@ -587,6 +589,7 @@ class _MessageListViewState extends State { message, messages, streamChannel, + i, ); } else if (i == bottomMessageIndex) { messageWidget = _buildBottomMessage( @@ -594,6 +597,7 @@ class _MessageListViewState extends State { message, messages, streamChannel!, + i, ); } else { if (widget.messageBuilder != null) { @@ -621,7 +625,8 @@ class _MessageListViewState extends State { }, ), if (widget.showScrollToBottom) _buildScrollToBottom(), - if (widget.showFloatingDateDivider) _buildFloatingDateDivider(), + if (widget.showFloatingDateDivider) + _buildFloatingDateDivider(itemCount), ], ); } @@ -647,42 +652,34 @@ class _MessageListViewState extends State { ); } - Positioned _buildFloatingDateDivider() => Positioned( + Positioned _buildFloatingDateDivider(int itemCount) => Positioned( top: 20, child: BetterStreamBuilder>( initialData: _itemPositionListener.itemPositions.value, stream: _itemPositionStream, comparator: (a, b) { - if (a == null) { + if (a == null || b == null) { return false; } - final aTop = _getTopElement(a)?.index; - final bTop = _getTopElement(b)?.index; + final aTop = _getTopElementIndex(a); + final bTop = _getTopElementIndex(b); return aTop == bTop; }, builder: (context, values) { - final items = _itemPositionListener.itemPositions.value; - if (items.isEmpty || messages.isEmpty) { - return const SizedBox(); + if (values.isEmpty || messages.isEmpty) { + return const Offstage(); } - var index = _getTopElement(values)?.index; + final index = _getTopElementIndex(values); - if (index == null || index > messages.length) { - return const SizedBox(); - } - - if (index == messages.length) { - index = max(index - 1, 0); + if (index == null || index <= 2 || index >= itemCount - 3) { + return const Offstage(); } + final message = messages[index - 2]; return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder!( - messages[index].createdAt.toLocal(), - ) - : DateDivider( - dateTime: messages[index].createdAt.toLocal(), - ); + ? widget.dateDividerBuilder!(message.createdAt.toLocal()) + : DateDivider(dateTime: message.createdAt.toLocal()); }, ), ); @@ -691,16 +688,13 @@ class _MessageListViewState extends State { StreamChannelState? channel, QueryDirection direction) => _messageListController.paginateData!(direction: direction); - ItemPosition? _getTopElement(Iterable values) { - final inView = - values.where((ItemPosition position) => position.itemLeadingEdge < 0.9); - - if (inView.isEmpty) { - return null; - } - - return inView.reduce((ItemPosition max, ItemPosition position) => - position.itemLeadingEdge > max.itemLeadingEdge ? position : max); + int? _getTopElementIndex(Iterable values) { + final inView = values.where((position) => position.itemLeadingEdge < 1); + if (inView.isEmpty) return null; + return inView + .reduce((max, position) => + position.itemLeadingEdge > max.itemLeadingEdge ? position : max) + .index; } Widget _buildScrollToBottom() => StreamBuilder>( @@ -796,6 +790,7 @@ class _MessageListViewState extends State { Message message, List messages, StreamChannelState? streamChannel, + int index, ) { Widget messageWidget; if (widget.messageBuilder != null) { @@ -813,7 +808,7 @@ class _MessageListViewState extends State { ), ); } else { - messageWidget = buildMessage(message, messages, messages.length - 1); + messageWidget = buildMessage(message, messages, index); } return messageWidget; } @@ -823,6 +818,7 @@ class _MessageListViewState extends State { Message message, List messages, StreamChannelState streamChannel, + int index, ) { Widget messageWidget; if (widget.messageBuilder != null) { @@ -840,7 +836,7 @@ class _MessageListViewState extends State { ), ); } else { - messageWidget = buildMessage(message, messages, 0); + messageWidget = buildMessage(message, messages, index); } return VisibilityDetector( @@ -952,7 +948,7 @@ class _MessageListViewState extends State { final userId = StreamChat.of(context).user!.id; final isMyMessage = message.user!.id == userId; - final nextMessage = index - 2 >= 0 ? messages[index - 2] : null; + final nextMessage = index - 3 >= 0 ? messages[index - 3] : null; final isNextUserSame = nextMessage != null && message.user!.id == nextMessage.user!.id; diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index ddf342dc..871a8a1d 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -25,7 +25,7 @@ class BetterStreamBuilder extends StatefulWidget { final T initialData; /// Comparator used to check if the new data is different than the last one - final bool Function(T?, T)? comparator; + final bool Function(T?, T?)? comparator; /// Builder that builds based on the new snapshot final Widget Function(BuildContext context, T data) builder; @@ -96,9 +96,8 @@ class _BetterStreamBuilderState extends State> { void _onEvent(T event) { _lastError = null; - final isEqual = widget.comparator != null - ? widget.comparator!(_lastEvent, event) - : event == _lastEvent; + final isEqual = + widget.comparator?.call(_lastEvent, event) ?? event == _lastEvent; if (!isEqual) { if (mounted) { setState(() {}); From c169b887af526f4d097e8fc5bf2dc7bc004b6e7d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 24 Jun 2021 18:24:56 +0530 Subject: [PATCH 4/5] add docs Signed-off-by: Sahil Kumar --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 3194827d..85c441cd 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -238,10 +238,10 @@ class MessageListView extends StatefulWidget { /// Function called when messages are fetched final Widget Function(BuildContext, List)? messageListBuilder; - /// + /// Function used to build a header widget final WidgetBuilder? headerBuilder; - /// + /// Function used to build a footer widget final WidgetBuilder? footerBuilder; /// Function used to build a loading widget From 5325b7cb5ed55f72f53fcac81cfacbf4d9cafd93 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 24 Jun 2021 18:25:59 +0530 Subject: [PATCH 5/5] fix analyzer warning Signed-off-by: Sahil Kumar --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 85c441cd..52ea3ab9 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1,6 +1,5 @@ // ignore_for_file: lines_longer_than_80_chars import 'dart:async'; -import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart';