From 390062f1c20879183578be66a59a7f6ce5521cdb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 2 Dec 2020 19:30:24 +0530 Subject: [PATCH 01/24] feat: 2 way pagination Signed-off-by: Sahil Kumar --- example/lib/main.dart | 230 +++++++++++++++-------------- example/lib/routes/app_routes.dart | 8 +- lib/src/message_list_view.dart | 19 ++- lib/src/stream_channel.dart | 98 +++++++++--- pubspec.yaml | 5 +- 5 files changed, 219 insertions(+), 141 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 4b529e23..f1667139 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -294,130 +294,136 @@ class _ChannelListPageState extends State { @override Widget build(BuildContext context) { final user = StreamChat.of(context).user; - return ChannelsBloc( - child: MessageSearchBloc( - child: Column( - children: [ - SearchTextField( - controller: _controller, - showCloseButton: _isSearchActive, - ), - Expanded( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: _isSearchActive - ? MessageSearchListView( - messageQuery: _channelQuery, - filters: { - 'members': { - r'$in': [user.id] - } - }, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - paginationParams: PaginationParams(limit: 20), - onItemTap: (message) {}, - ) - : ChannelListView( - onStartChatPressed: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - swipeToAction: true, - filter: { - 'members': { - r'$in': [user.id], + return WillPopScope( + onWillPop: () async { + if (_isSearchActive) { + _controller.clear(); + setState(() => _isSearchActive = false); + return false; + } + return true; + }, + child: ChannelsBloc( + child: MessageSearchBloc( + child: Column( + children: [ + SearchTextField( + controller: _controller, + showCloseButton: _isSearchActive, + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: _isSearchActive + ? MessageSearchListView( + messageQuery: _channelQuery, + filters: { + 'members': { + r'$in': [user.id] + } }, - }, - options: { - 'presence': true, - }, - pagination: PaginationParams( - limit: 20, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onItemTap: (messageResponse) async { + final client = StreamChat.of(context).client; + final message = messageResponse.message; + final channel = Channel.fromState( + client, + ChannelState( + channel: messageResponse.channel, + messages: [message], + ), + ); + await Future.wait([ + channel.query( + messagesPagination: PaginationParams( + lessThan: message.id, + limit: 25, + ), + preferOffline: true, + ), + channel.query( + messagesPagination: PaginationParams( + greaterThan: message.id, + limit: 25, + ), + preferOffline: true, + ), + ]); + final messages = channel.state.messages; + final totalMessages = messages.length; + final messageIndex = messages + .indexWhere((e) => e.id == message.id); + final initialIndex = totalMessages - messageIndex; + final bool isFirstMessage = messageIndex == 0; + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialScrollIndex: initialIndex, + initialAlignment: isFirstMessage ? 0 : 0.5, + ), + ); + }, + ) + : ChannelListView( + onStartChatPressed: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); + }, + swipeToAction: true, + filter: { + 'members': { + r'$in': [user.id], + }, + }, + options: { + 'presence': true, + }, + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), ), - channelWidget: ChannelPage(), - ), + ), ), ), - ), - ], + ], + ), ), ), ); } } -class ChannelQuerySearchResultPage extends StatelessWidget { - final Stream> searchResultStream; +class ChannelPageArgs { + final Channel channel; + final int initialScrollIndex; + final double initialAlignment; - const ChannelQuerySearchResultPage({ - Key key, - @required this.searchResultStream, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return StreamBuilder>( - initialData: const [], - stream: searchResultStream, - builder: (context, snapshot) { - final result = snapshot.data; - return Column( - children: [ - if (result.isNotEmpty) - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black.withOpacity(0.02), - Colors.white.withOpacity(0.05), - ], - stops: [0, 1], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - '${result.length} results', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - ), - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: result.length, - itemBuilder: (context, index) { - return ListTile( - leading: UserAvatar(), - title: Text(result[index].toJson().toString()), - ); - }, - ), - ), - ], - ); - }, - ); - } + const ChannelPageArgs({ + this.channel, + this.initialScrollIndex = 0, + this.initialAlignment = 0, + }); } class ChannelPage extends StatelessWidget { + final int initialScrollIndex; + final double initialAlignment; + const ChannelPage({ Key key, + this.initialScrollIndex = 0, + this.initialAlignment = 0, }) : super(key: key); @override @@ -433,6 +439,8 @@ class ChannelPage extends StatelessWidget { child: Stack( children: [ MessageListView( + initialScrollIndex: initialScrollIndex, + initialAlignment: initialAlignment, threadBuilder: (_, parentMessage) { return ThreadPage( parent: parentMessage, @@ -467,10 +475,14 @@ class ChannelPage extends StatelessWidget { class ThreadPage extends StatelessWidget { final Message parent; + final int initialScrollIndex; + final double initialAlignment; ThreadPage({ Key key, this.parent, + this.initialScrollIndex = 0, + this.initialAlignment = 0, }) : super(key: key); @override @@ -484,6 +496,8 @@ class ThreadPage extends StatelessWidget { Expanded( child: MessageListView( parentMessage: parent, + initialScrollIndex: initialScrollIndex, + initialAlignment: initialAlignment, ), ), if (parent.type != 'deleted') diff --git a/example/lib/routes/app_routes.dart b/example/lib/routes/app_routes.dart index 6b0529e1..8ee907b8 100644 --- a/example/lib/routes/app_routes.dart +++ b/example/lib/routes/app_routes.dart @@ -34,9 +34,13 @@ class AppRoutes { return MaterialPageRoute( settings: const RouteSettings(name: Routes.CHANNEL_PAGE), builder: (_) { + final arg = args as ChannelPageArgs; return StreamChannel( - channel: args as Channel, - child: ChannelPage(), + channel: arg.channel, + child: ChannelPage( + initialScrollIndex: arg.initialScrollIndex, + initialAlignment: arg.initialAlignment, + ), ); }); case Routes.NEW_CHAT: diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 3feda8ce..f9e40bf7 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -480,8 +480,8 @@ class _MessageListViewState extends State { } else { streamChannel.getReplies(widget.parentMessage.id); } + _topWasVisible = !topIsVisible; } - _topWasVisible = topIsVisible; }, ); } @@ -515,14 +515,21 @@ class _MessageListViewState extends State { key: ValueKey('BOTTOM-MESSAGE'), onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; - if (isVisible && - !_bottomWasVisible && - streamChannel.channel.config?.readEvents == true) { - if (streamChannel.channel.state.unreadCount > 0) { + if (isVisible && !_bottomWasVisible) { + if (widget.parentMessage == null) { + streamChannel.queryMessages(direction: QueryDirection.bottom); + } else { + streamChannel.getReplies( + widget.parentMessage.id, + direction: QueryDirection.bottom, + ); + } + if (streamChannel.channel.config?.readEvents == true && + streamChannel.channel.state.unreadCount > 0) { streamChannel.channel.markRead(); } + _bottomWasVisible = !isVisible; } - _bottomWasVisible = isVisible; if (mounted) { setState(() { _showScrollToBottom = !isVisible; diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 6fafa070..5dbfd103 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; +enum QueryDirection { top, bottom } + /// Widget used to provide information about the channel to the widget tree /// /// Use [StreamChannel.of] to get the current [StreamChannelState] instance. @@ -52,34 +54,57 @@ class StreamChannelState extends State { /// The stream notifying the state of queryMessage call Stream get queryMessage => _queryMessageController.stream; - bool _paginationEnded = false; + bool _topPaginationEnded = false; + bool _bottomPaginationEnded = false; /// Calls [channel.query] updating [queryMessage] stream - void queryMessages() { - if (_queryMessageController.value == true || _paginationEnded) { + void queryMessages({QueryDirection direction = QueryDirection.top}) { + if (_queryMessageController.value == true || + (_topPaginationEnded && _bottomPaginationEnded)) { return; } _queryMessageController.add(true); - String firstId; - if (channel.state.messages.isNotEmpty) { - firstId = channel.state.messages.first.id; - } + String id; + PaginationParams params; - final messageLimit = 50; + final messageLimit = 25; + + if (channel.state.messages.isNotEmpty) { + switch (direction) { + case QueryDirection.top: + id = channel.state.messages.first.id; + params = PaginationParams( + lessThan: id, + limit: messageLimit, + ); + break; + case QueryDirection.bottom: + id = channel.state.messages.last.id; + params = PaginationParams( + greaterThan: id, + limit: messageLimit, + ); + break; + } + } widget.channel .query( - messagesPagination: PaginationParams( - lessThan: firstId, - limit: messageLimit, - ), + messagesPagination: params, preferOffline: true, ) .then((res) { if (res.messages.isEmpty || res.messages.length < messageLimit) { - _paginationEnded = true; + switch (direction) { + case QueryDirection.top: + _topPaginationEnded = true; + break; + case QueryDirection.bottom: + _bottomPaginationEnded = true; + break; + } } _queryMessageController.add(false); }).catchError((e, stack) { @@ -90,35 +115,60 @@ class StreamChannelState extends State { } /// Calls [channel.getReplies] updating [queryMessage] stream - Future getReplies(String parentId) async { - if (_queryMessageController.value == true || _paginationEnded) { + Future getReplies( + String parentId, { + QueryDirection direction = QueryDirection.top, + }) async { + if (_queryMessageController.value == true || + (_topPaginationEnded && _bottomPaginationEnded)) { return; } _queryMessageController.add(true); - String firstId; + String id; + PaginationParams params; + + final messageLimit = 50; + if (widget.channel.state.threads.containsKey(parentId)) { final thread = widget.channel.state.threads[parentId]; - if (thread != null && thread.isNotEmpty) { - firstId = thread?.first?.id; + switch (direction) { + case QueryDirection.top: + id = thread?.first?.id; + params = PaginationParams( + lessThan: id, + limit: messageLimit, + ); + break; + case QueryDirection.bottom: + id = thread?.last?.id; + params = PaginationParams( + greaterThan: id, + limit: messageLimit, + ); + break; + } } } - final messageLimit = 50; return widget.channel .getReplies( parentId, - PaginationParams( - lessThan: firstId, - limit: messageLimit, - ), + params, preferOffline: true, ) .then((res) { if (res.messages.isEmpty || res.messages.length < messageLimit) { - _paginationEnded = true; + switch (direction) { + case QueryDirection.top: + _topPaginationEnded = true; + break; + case QueryDirection.bottom: + _bottomPaginationEnded = true; + break; + } } _queryMessageController.add(false); }).catchError((e, stack) { diff --git a/pubspec.yaml b/pubspec.yaml index 75195a30..1d2a299e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,10 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.14 + stream_chat: + git: + url: https://github.com/GetStream/stream-chat-dart + ref: two-way-pagination mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 546fdaf4874658697f13df4f1e5bd91c706ce39b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 2 Dec 2020 19:30:55 +0530 Subject: [PATCH 02/24] [LazyLoadScrollView] : Add support of onStartOfPage Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 49 ++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index e83fc777..5ca13d07 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -1,18 +1,18 @@ import 'package:flutter/widgets.dart'; -enum LoadingStatus { LOADING, STABLE } +enum _LoadingStatus { LOADING, STABLE } -/// Signature for EndOfPageListeners -typedef EndOfPageListenerCallback = void Function(); - -/// A widget that wraps a [Widget] and will trigger [onEndOfPage] when it -/// reaches the bottom of the list +/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it +/// reaches the bottom/start of the list class LazyLoadScrollView extends StatefulWidget { /// The [Widget] that this widget watches for changes on final Widget child; + /// Called when the [child] reaches the start of the list + final VoidCallback onStartOfPage; + /// Called when the [child] reaches the end of the list - final EndOfPageListenerCallback onEndOfPage; + final VoidCallback onEndOfPage; /// The offset to take into account when triggering [onEndOfPage] in pixels final int scrollOffset; @@ -20,14 +20,15 @@ class LazyLoadScrollView extends StatefulWidget { /// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder final bool isLoading; + /// Initiates a LazyLoadScrollView widget LazyLoadScrollView({ Key key, @required this.child, - @required this.onEndOfPage, + this.onStartOfPage, + this.onEndOfPage, this.isLoading = false, this.scrollOffset = 100, - }) : assert(onEndOfPage != null), - assert(child != null), + }) : assert(child != null), super(key: key); @override @@ -35,13 +36,13 @@ class LazyLoadScrollView extends StatefulWidget { } class _LazyLoadScrollViewState extends State { - LoadingStatus _loadMoreStatus = LoadingStatus.STABLE; + _LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE; @override void didUpdateWidget(LazyLoadScrollView oldWidget) { super.didUpdateWidget(oldWidget); if (!widget.isLoading) { - _loadMoreStatus = LoadingStatus.STABLE; + _loadMoreStatus = _LoadingStatus.STABLE; } } @@ -59,21 +60,37 @@ class _LazyLoadScrollViewState extends State { notification.metrics.maxScrollExtent - notification.metrics.pixels <= widget.scrollOffset) { if (_loadMoreStatus != null && - _loadMoreStatus == LoadingStatus.STABLE) { - _loadMoreStatus = LoadingStatus.LOADING; + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; widget.onEndOfPage(); } } + if (notification.metrics.minScrollExtent < notification.metrics.pixels && + notification.metrics.pixels - notification.metrics.minScrollExtent <= + widget.scrollOffset) { + if (_loadMoreStatus != null && + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + widget.onStartOfPage(); + } + } return true; } if (notification is OverscrollNotification) { if (notification.overscroll > 0) { if (_loadMoreStatus != null && - _loadMoreStatus == LoadingStatus.STABLE) { - _loadMoreStatus = LoadingStatus.LOADING; + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; widget.onEndOfPage(); } } + if (notification.overscroll < 0) { + if (_loadMoreStatus != null && + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + widget.onStartOfPage(); + } + } return true; } return false; From 759fe5510ee96c089b596bb5ed70710f9b1999dd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Dec 2020 14:28:35 +0530 Subject: [PATCH 03/24] [MessageSearchListView] : Minor improvements. Signed-off-by: Sahil Kumar --- lib/src/message_search_list_view.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart index 8b346570..087ae839 100644 --- a/lib/src/message_search_list_view.dart +++ b/lib/src/message_search_list_view.dart @@ -14,6 +14,10 @@ typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); typedef MessageSearchItemBuilder = Widget Function( BuildContext, GetMessageResponse); +/// Builder used when [MessageSearchListView] is empty +typedef EmptyMessageSearchBuilder = Widget Function( + BuildContext context, String searchQuery); + /// /// It shows the list of searched messages. /// @@ -85,7 +89,7 @@ class MessageSearchListView extends StatefulWidget { final MessageSearchItemTapCallback onItemTap; /// The builder used when the channel list is empty. - final WidgetBuilder emptyBuilder; + final EmptyMessageSearchBuilder emptyBuilder; /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -250,11 +254,10 @@ class _MessageSearchListViewState extends State { final items = snapshot.data; - if (items.isEmpty && widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - - if (items.isEmpty && widget.emptyBuilder == null) { + if (items.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context, widget.messageQuery); + } return LayoutBuilder( builder: (context, viewportConstraints) { return SingleChildScrollView( From 4373888ae63fb2d7168b7fd895980d6b1f2b5b4a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 3 Dec 2020 10:49:41 +0100 Subject: [PATCH 04/24] jump scrollview on new messages --- lib/src/message_list_view.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index f9e40bf7..803ce456 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -164,6 +164,7 @@ class _MessageListViewState extends State { Function _onThreadTap; bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; + int messageListLength; @override Widget build(BuildContext context) { @@ -203,6 +204,22 @@ class _MessageListViewState extends State { ); } + final newMessagesListLength = messages.length; + + if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && + messageListLength != null) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - messageListLength; + if (diff > 0) { + _scrollController.jumpTo( + index: first.index + diff, + alignment: first.itemLeadingEdge, + ); + } + } + + messageListLength = newMessagesListLength; + return Stack( alignment: Alignment.center, children: [ From f367e6b7a8e6273c2bfa8e6652c2c34b106dc825 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Dec 2020 15:46:58 +0530 Subject: [PATCH 05/24] minor changes Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 803ce456..88e33ddc 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -164,7 +164,7 @@ class _MessageListViewState extends State { Function _onThreadTap; bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; - int messageListLength; + int _messageListLength; @override Widget build(BuildContext context) { @@ -207,9 +207,9 @@ class _MessageListViewState extends State { final newMessagesListLength = messages.length; if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && - messageListLength != null) { + _messageListLength != null) { final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - messageListLength; + final diff = newMessagesListLength - _messageListLength; if (diff > 0) { _scrollController.jumpTo( index: first.index + diff, @@ -218,7 +218,7 @@ class _MessageListViewState extends State { } } - messageListLength = newMessagesListLength; + _messageListLength = newMessagesListLength; return Stack( alignment: Alignment.center, From 5cd9653c34093087f891e1c8729136d61ef1e679 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Dec 2020 17:28:38 +0530 Subject: [PATCH 06/24] [MessageListView] : Use notification listener to implement pagination. Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 2 +- lib/src/message_list_view.dart | 230 ++++++++++++++--------------- 2 files changed, 114 insertions(+), 118 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index 5ca13d07..5cfeea61 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -15,7 +15,7 @@ class LazyLoadScrollView extends StatefulWidget { final VoidCallback onEndOfPage; /// The offset to take into account when triggering [onEndOfPage] in pixels - final int scrollOffset; + final double scrollOffset; /// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder final bool isLoading; diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 88e33ddc..cbe09a6f 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/message_widget.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/system_message.dart'; @@ -160,7 +161,6 @@ class MessageListView extends StatefulWidget { class _MessageListViewState extends State { ItemScrollController _scrollController; bool _bottomWasVisible = false; - bool _topWasVisible = false; Function _onThreadTap; bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; @@ -223,110 +223,121 @@ class _MessageListViewState extends State { return Stack( alignment: Alignment.center, children: [ - ScrollablePositionedList.builder( - itemPositionsListener: _itemPositionListener, - addAutomaticKeepAlives: true, - key: Key('messageListView'), - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - itemCount: messages.length + - 1 + - (widget.parentMessage != null ? 1 : 0), - itemBuilder: (context, i) { - if (i == messages.length + 1) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( + LazyLoadScrollView( + onStartOfPage: () => _paginateData( + streamChannel, + QueryDirection.bottom, + ), + onEndOfPage: () => _paginateData( + streamChannel, + QueryDirection.top, + ), + child: ScrollablePositionedList.builder( + itemPositionsListener: _itemPositionListener, + addAutomaticKeepAlives: true, + key: Key('messageListView'), + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + itemCount: messages.length + + 1 + + (widget.parentMessage != null ? 1 : 0), + itemBuilder: (context, i) { + if (i == messages.length + 1) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, + widget.parentMessage, + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildParentMessage(widget.parentMessage), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Start of thread', + textAlign: TextAlign.center, + ), + color: + Theme.of(context).accentColor.withAlpha(50), + ), + ), + ], + ); + } + } + + if (i == messages.length) { + return _buildLoadingIndicator(streamChannel); + } + final message = messages[i]; + final nextMessage = i > 0 ? messages[i - 1] : null; + + Widget messageWidget; + + if (i == 0) { + messageWidget = _buildBottomMessage( context, - widget.parentMessage, + message, + messages, + streamChannel, + ); + } else if (i == messages.length - 1) { + messageWidget = _buildTopMessage( + context, + message, + messages, + streamChannel, ); } else { + if (widget.messageBuilder != null) { + messageWidget = Builder( + key: ValueKey('MESSAGE-${message.id}'), + builder: (_) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + messages, + i, + ), + messages), + ); + } else { + messageWidget = buildMessage(message, messages, i); + } + } + + if (nextMessage != null && + !Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), Units.DAY)) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - buildParentMessage(widget.parentMessage), + messageWidget, Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - 'Start of thread', - textAlign: TextAlign.center, - ), - color: - Theme.of(context).accentColor.withAlpha(50), - ), + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal()) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ), ), ], ); } - } - if (i == messages.length) { - return _buildLoadingIndicator(streamChannel); - } - final message = messages[i]; - final nextMessage = i > 0 ? messages[i - 1] : null; - - Widget messageWidget; - - if (i == 0) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel, - ); - } else if (i == messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - messages, - streamChannel, - ); - } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = buildMessage(message, messages, i); - } - } - - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()) - .isSame(nextMessage.createdAt.toLocal(), Units.DAY)) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - messageWidget, - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ), - ], - ); - } - - return messageWidget; - }, + return messageWidget; + }, + ), ), if (widget.showScrollToBottom && _showScrollToBottom) _buildScrollToBottom(), @@ -365,6 +376,14 @@ class _MessageListViewState extends State { }); } + void _paginateData(StreamChannelState channel, QueryDirection direction) { + if (widget.parentMessage == null) { + channel.queryMessages(direction: direction); + } else { + channel.getReplies(widget.parentMessage.id, direction: direction); + } + } + ItemPosition _getTopElement(Iterable values) { return values .where((ItemPosition position) => position.itemLeadingEdge < 0.9) @@ -485,22 +504,7 @@ class _MessageListViewState extends State { } else { messageWidget = buildMessage(message, messages, messages.length - 1); } - - return VisibilityDetector( - key: ValueKey('TOP-MESSAGE'), - child: messageWidget, - onVisibilityChanged: (visibility) { - final topIsVisible = visibility.visibleBounds != Rect.zero; - if (topIsVisible && !_topWasVisible) { - if (widget.parentMessage == null) { - streamChannel.queryMessages(); - } else { - streamChannel.getReplies(widget.parentMessage.id); - } - _topWasVisible = !topIsVisible; - } - }, - ); + return messageWidget; } Widget _buildBottomMessage( @@ -533,14 +537,6 @@ class _MessageListViewState extends State { onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; if (isVisible && !_bottomWasVisible) { - if (widget.parentMessage == null) { - streamChannel.queryMessages(direction: QueryDirection.bottom); - } else { - streamChannel.getReplies( - widget.parentMessage.id, - direction: QueryDirection.bottom, - ); - } if (streamChannel.channel.config?.readEvents == true && streamChannel.channel.state.unreadCount > 0) { streamChannel.channel.markRead(); From 7495acee3ee5d6c9fa3e7589ac3f7f45d518e86b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 16:56:09 +0530 Subject: [PATCH 07/24] changes Signed-off-by: Sahil Kumar --- example/lib/choose_user_page.dart | 4 +- example/lib/group_chat_details_screen.dart | 2 +- example/lib/main.dart | 47 +-- example/lib/new_chat_screen.dart | 2 +- example/lib/routes/app_routes.dart | 4 +- lib/src/lazy_load_scroll_view.dart | 37 ++- lib/src/message_input.dart | 10 +- lib/src/message_list_view.dart | 359 +++++++++++++-------- lib/src/stream_channel.dart | 351 ++++++++++++-------- lib/src/user_list_view.dart | 4 +- pubspec.yaml | 7 +- 11 files changed, 504 insertions(+), 323 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index a64013b9..73b12425 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -13,13 +13,13 @@ import 'routes/routes.dart'; const kStreamApiKey = 'STREAM_API_KEY'; const kStreamUserId = 'STREAM_USER_ID'; const kStreamToken = 'STREAM_TOKEN'; -const kDefaultStreamApiKey = 'uj7qrdbfrzvg'; +const kDefaultStreamApiKey = 's2dxdhpxd94g'; class ChooseUserPage extends StatelessWidget { @override Widget build(BuildContext context) { final users = { - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI': + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg': User( id: 'vishal', extraData: { diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart index d759a82e..c6d4331d 100644 --- a/example/lib/group_chat_details_screen.dart +++ b/example/lib/group_chat_details_screen.dart @@ -133,7 +133,7 @@ class _GroupChatDetailsScreenState extends State { context, Routes.CHANNEL_PAGE, ModalRoute.withName(Routes.HOME), - arguments: channel, + arguments: ChannelPageArgs(channel: channel), ); }, ), diff --git a/example/lib/main.dart b/example/lib/main.dart index f1667139..10f6d5b3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -337,40 +337,14 @@ class _ChannelListPageState extends State { final message = messageResponse.message; final channel = Channel.fromState( client, - ChannelState( - channel: messageResponse.channel, - messages: [message], - ), + ChannelState(channel: messageResponse.channel), ); - await Future.wait([ - channel.query( - messagesPagination: PaginationParams( - lessThan: message.id, - limit: 25, - ), - preferOffline: true, - ), - channel.query( - messagesPagination: PaginationParams( - greaterThan: message.id, - limit: 25, - ), - preferOffline: true, - ), - ]); - final messages = channel.state.messages; - final totalMessages = messages.length; - final messageIndex = messages - .indexWhere((e) => e.id == message.id); - final initialIndex = totalMessages - messageIndex; - final bool isFirstMessage = messageIndex == 0; Navigator.pushNamed( context, Routes.CHANNEL_PAGE, arguments: ChannelPageArgs( channel: channel, - initialScrollIndex: initialIndex, - initialAlignment: isFirstMessage ? 0 : 0.5, + initialMessage: message, ), ); }, @@ -406,24 +380,24 @@ class _ChannelListPageState extends State { class ChannelPageArgs { final Channel channel; - final int initialScrollIndex; - final double initialAlignment; + final Message initialMessage; const ChannelPageArgs({ this.channel, - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialMessage, }); } class ChannelPage extends StatelessWidget { final int initialScrollIndex; final double initialAlignment; + final bool highlightInitialMessage; const ChannelPage({ Key key, - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialScrollIndex, + this.initialAlignment, + this.highlightInitialMessage = false, }) : super(key: key); @override @@ -441,6 +415,7 @@ class ChannelPage extends StatelessWidget { MessageListView( initialScrollIndex: initialScrollIndex, initialAlignment: initialAlignment, + highlightInitialMessage: highlightInitialMessage, threadBuilder: (_, parentMessage) { return ThreadPage( parent: parentMessage, @@ -481,8 +456,8 @@ class ThreadPage extends StatelessWidget { ThreadPage({ Key key, this.parent, - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialScrollIndex, + this.initialAlignment, }) : super(key: key); @override diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 5fbfbf07..94c7f866 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -339,7 +339,7 @@ class _NewChatScreenState extends State { context, Routes.CHANNEL_PAGE, ModalRoute.withName(Routes.HOME), - arguments: channel, + arguments: ChannelPageArgs(channel: channel), ); } } diff --git a/example/lib/routes/app_routes.dart b/example/lib/routes/app_routes.dart index 8ee907b8..8082245f 100644 --- a/example/lib/routes/app_routes.dart +++ b/example/lib/routes/app_routes.dart @@ -37,9 +37,9 @@ class AppRoutes { final arg = args as ChannelPageArgs; return StreamChannel( channel: arg.channel, + initialMessageId: arg.initialMessage?.id, child: ChannelPage( - initialScrollIndex: arg.initialScrollIndex, - initialAlignment: arg.initialAlignment, + highlightInitialMessage: arg.initialMessage != null, ), ); }); diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index 5cfeea61..a3c344aa 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; enum _LoadingStatus { LOADING, STABLE } @@ -9,10 +10,10 @@ class LazyLoadScrollView extends StatefulWidget { final Widget child; /// Called when the [child] reaches the start of the list - final VoidCallback onStartOfPage; + final AsyncCallback onStartOfPage; /// Called when the [child] reaches the end of the list - final VoidCallback onEndOfPage; + final AsyncCallback onEndOfPage; /// The offset to take into account when triggering [onEndOfPage] in pixels final double scrollOffset; @@ -38,14 +39,6 @@ class LazyLoadScrollView extends StatefulWidget { class _LazyLoadScrollViewState extends State { _LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE; - @override - void didUpdateWidget(LazyLoadScrollView oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.isLoading) { - _loadMoreStatus = _LoadingStatus.STABLE; - } - } - @override Widget build(BuildContext context) { return NotificationListener( @@ -62,7 +55,11 @@ class _LazyLoadScrollViewState extends State { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onEndOfPage(); + if (widget.onEndOfPage != null) { + widget.onEndOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } if (notification.metrics.minScrollExtent < notification.metrics.pixels && @@ -71,7 +68,11 @@ class _LazyLoadScrollViewState extends State { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onStartOfPage(); + if (widget.onStartOfPage != null) { + widget.onStartOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } return true; @@ -81,14 +82,22 @@ class _LazyLoadScrollViewState extends State { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onEndOfPage(); + if (widget.onEndOfPage != null) { + widget.onEndOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } if (notification.overscroll < 0) { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onStartOfPage(); + if (widget.onStartOfPage != null) { + widget.onStartOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } return true; diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 4e30afe1..e38b9af3 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -21,7 +21,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; -import 'package:photo_manager/photo_manager.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -436,6 +435,7 @@ class MessageInputState extends State { } Timer _debounce; + void _onChanged(BuildContext context, String s) { if (_debounce?.isActive == true) _debounce.cancel(); _debounce = Timer( @@ -1859,7 +1859,8 @@ class MessageInputState extends State { _mentionsOverlay?.remove(); _mentionsOverlay = null; - final channel = StreamChannel.of(context).channel; + final streamChannel = StreamChannel.of(context); + final channel = streamChannel.channel; Future sendingFuture; Message message; @@ -1885,6 +1886,10 @@ class MessageInputState extends State { message = await widget.preMessageSending(message); } + if (!channel.state.isUpToDate) { + await streamChannel.reloadChannel(); + } + if (widget.editMessage == null || widget.editMessage.status == MessageSendingStatus.FAILED) { sendingFuture = channel.sendMessage(message); @@ -1970,6 +1975,7 @@ class MessageInputState extends State { } bool _initialized = false; + @override void didChangeDependencies() { if (widget.editMessage != null && !_initialized) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 5efdb36a..042e20b9 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:math'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; @@ -109,10 +111,11 @@ class MessageListView extends StatefulWidget { this.onThreadTap, this.dateDividerBuilder, this.scrollPhysics = const AlwaysScrollableScrollPhysics(), - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialScrollIndex, + this.initialAlignment, this.scrollController, this.itemPositionListener, + this.highlightInitialMessage = false, }) : super(key: key); /// Function used to build a custom message widget @@ -154,6 +157,11 @@ class MessageListView extends StatefulWidget { /// The ScrollPhysics used by the ListView final ScrollPhysics scrollPhysics; + /// If true the list will highlight the initialMessage if there is any. + /// + /// Also See [StreamChannel] + final bool highlightInitialMessage; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -166,6 +174,51 @@ class _MessageListViewState extends State { ItemPositionsListener _itemPositionListener; int _messageListLength; + int get _initialIndex { + if (widget.initialScrollIndex != null) return widget.initialScrollIndex; + final streamChannel = StreamChannel.of(context); + if (streamChannel.initialMessageId != null) { + final messages = streamChannel.channel.state.messages; + final totalMessages = messages.length; + final messageIndex = messages.indexWhere((e) { + return e.id == streamChannel.initialMessageId; + }); + return totalMessages - messageIndex - 1; + } + return 0; + } + + double get _initialAlignment { + if (widget.initialAlignment != null) return widget.initialAlignment; + final streamChannel = StreamChannel.of(context); + if (streamChannel.initialMessageId != null) { + final messages = streamChannel.channel.state.messages; + final messageIndex = messages.indexWhere((e) { + return e.id == streamChannel.initialMessageId; + }); + final isFirstMessage = messageIndex == 0; + return isFirstMessage ? 0 : 0.5; + } + return 0; + } + + bool _isInitialMessage(String id) { + final streamChannel = StreamChannel.of(context); + return streamChannel.initialMessageId == id; + } + + bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate; + + bool _topPaginationActive = false; + bool _bottomPaginationActive = false; + + bool get _paginationActive => _topPaginationActive || _bottomPaginationActive; + + int initialIndex; + double initialAlignment; + + List messages = []; + @override Widget build(BuildContext context) { final streamChannel = StreamChannel.of(context); @@ -176,6 +229,11 @@ class _MessageListViewState extends State { .map((threads) => threads[widget.parentMessage.id]) : streamChannel.channel.state.messagesStream; + if (!_paginationActive && !_upToDate) { + initialIndex = _initialIndex; + initialAlignment = _initialAlignment; + } + return StreamBuilder>( stream: messagesStream.map((messages) => messages .where((e) => @@ -190,32 +248,40 @@ class _MessageListViewState extends State { ); } - final messages = snapshot.data?.reversed?.toList() ?? []; + final messageList = snapshot.data?.reversed?.toList() ?? []; - if (messages.isEmpty) { - return Center( - child: Text( - 'No chats here yet...', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(.5), + if (messageList.isEmpty) { + if (_upToDate) { + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), ), - ), - ); + ); + } + } else { + messages = messageList; } final newMessagesListLength = messages.length; - if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && - _messageListLength != null) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - _scrollController.jumpTo( - index: first.index + diff, - alignment: first.itemLeadingEdge, - ); + if (_bottomPaginationActive) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && + _messageListLength != null) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; + } } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; } _messageListLength = newMessagesListLength; @@ -224,28 +290,32 @@ class _MessageListViewState extends State { alignment: Alignment.center, children: [ LazyLoadScrollView( - onStartOfPage: () => _paginateData( - streamChannel, - QueryDirection.bottom, - ), - onEndOfPage: () => _paginateData( - streamChannel, - QueryDirection.top, - ), + onStartOfPage: () async { + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; + _paginateData(streamChannel, QueryDirection.bottom); + } + }, + onEndOfPage: () async { + _topPaginationActive = true; + _bottomPaginationActive = false; + _paginateData(streamChannel, QueryDirection.top); + }, child: ScrollablePositionedList.builder( + key: ValueKey(initialIndex + initialAlignment), itemPositionsListener: _itemPositionListener, addAutomaticKeepAlives: true, - key: Key('messageListView'), - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: true, itemCount: messages.length + - 1 + + 2 + (widget.parentMessage != null ? 1 : 0), itemBuilder: (context, i) { - if (i == messages.length + 1) { + if (i == messages.length + 2) { if (widget.parentMessageBuilder != null) { return widget.parentMessageBuilder( context, @@ -273,16 +343,24 @@ class _MessageListViewState extends State { ); } } - - if (i == messages.length) { - return _buildLoadingIndicator(streamChannel); + if (i == messages.length + 1) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.top, + ); } - final message = messages[i]; - final nextMessage = i > 0 ? messages[i - 1] : null; + if (i == 0) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.bottom, + ); + } + final message = messages[i - 1]; + final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; Widget messageWidget; - if (i == 0) { + if (i == 1) { messageWidget = _buildBottomMessage( context, message, @@ -339,8 +417,7 @@ class _MessageListViewState extends State { }, ), ), - if (widget.showScrollToBottom && _showScrollToBottom) - _buildScrollToBottom(), + if (widget.showScrollToBottom) _buildScrollToBottom(), Positioned( top: 20.0, child: ValueListenableBuilder>( @@ -380,7 +457,7 @@ class _MessageListViewState extends State { if (widget.parentMessage == null) { channel.queryMessages(direction: direction); } else { - channel.getReplies(widget.parentMessage.id, direction: direction); + channel.getReplies(widget.parentMessage.id); } } @@ -393,91 +470,117 @@ class _MessageListViewState extends State { Widget _buildScrollToBottom() { final streamChannel = StreamChannel.of(context); - return Positioned( - bottom: 8, - right: 8, - width: 40, - height: 40, - child: Stack( - clipBehavior: Clip.none, - children: [ - FloatingActionButton( - backgroundColor: Colors.white, - child: StreamSvgIcon.down( - color: Colors.black, - ), - onPressed: () { - setState(() { - _showScrollToBottom = false; - }); - _scrollController.scrollTo( - index: 0, - duration: Duration(seconds: 1), - curve: Curves.easeInOut, - ); - }, - ), - if (streamChannel.channel.state.members.any((Member e) => - e.userId == streamChannel.channel.client.state.user.id)) - StreamBuilder( - stream: streamChannel.channel.state.unreadCountStream, - initialData: streamChannel.channel.state.unreadCount, - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data <= 0) { - return Offstage(); + return StreamBuilder>( + stream: Rx.combineLatest2( + streamChannel.channel.state.isUpToDateStream, + streamChannel.channel.state.unreadCountStream, + (bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount), + ), + builder: (_, snapshot) { + if (snapshot.hasError) { + return Offstage(); + } else if (!snapshot.hasData) { + return Offstage(); + } + final isUpToDate = snapshot.data.item1; + final showScrollToBottom = !isUpToDate || _showScrollToBottom; + if (!showScrollToBottom) { + return Offstage(); + } + final unreadCount = snapshot.data.item2; + final showUnreadCount = unreadCount > 0 && + streamChannel.channel.state.members.any( + (e) => e.userId == streamChannel.channel.client.state.user.id); + return Positioned( + bottom: 8, + right: 8, + width: 40, + height: 40, + child: Stack( + clipBehavior: Clip.none, + children: [ + FloatingActionButton( + backgroundColor: Colors.white, + child: StreamSvgIcon.down( + color: Colors.black, + ), + onPressed: () { + if (!_upToDate) { + _bottomPaginationActive = false; + _topPaginationActive = false; + streamChannel.reloadChannel(); + } else { + setState(() => _showScrollToBottom = false); + _scrollController.scrollTo( + index: 0, + duration: Duration(seconds: 1), + curve: Curves.easeInOut, + ); } - return Positioned( - width: 20, - height: 20, - left: 10, - top: -10, - child: CircleAvatar( - child: Padding( - padding: const EdgeInsets.all(3.0), - child: Text( - snapshot.data.toString(), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - ), + }, + ), + if (showUnreadCount) + Positioned( + width: 20, + height: 20, + left: 10, + top: -10, + child: CircleAvatar( + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Text( + snapshot.data.toString(), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, ), ), ), - ); - }), - ], - ), + ), + ), + ], + ), + ); + }, ); } - Container _buildLoadingIndicator(StreamChannelState streamChannel) { - return Container( - key: Key('LOADING-INDICATOR'), - height: 50, - width: double.infinity, - child: StreamBuilder( - stream: streamChannel.queryMessage, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( - color: Color(0xffd0021B).withAlpha(26), - child: Center( - child: Text('Error loading messages'), - ), - ); - } - if (!snapshot.data) { - return SizedBox(); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: CircularProgressIndicator(), + Widget _buildLoadingIndicator( + StreamChannelState streamChannel, + QueryDirection direction, + ) { + final stream = direction == QueryDirection.top + ? streamChannel.queryTopMessages + : streamChannel.queryBottomMessages; + return StreamBuilder( + key: Key('LOADING-INDICATOR'), + stream: stream, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Container( + color: Color(0xffd0021B).withAlpha(26), + child: Center( + child: Text('Error loading messages'), ), ); - }), - ); + } + if (!snapshot.data) { + if (direction == QueryDirection.top) { + return Container( + height: 50, + width: double.infinity, + ); + } + return Offstage(); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: const CupertinoActivityIndicator(), + ), + ); + }); } Widget _buildTopMessage( @@ -544,9 +647,7 @@ class _MessageListViewState extends State { _bottomWasVisible = !isVisible; } if (mounted) { - setState(() { - _showScrollToBottom = !isVisible; - }); + setState(() => _showScrollToBottom = !isVisible); } }, child: messageWidget, @@ -602,7 +703,7 @@ class _MessageListViewState extends State { final userId = StreamChat.of(context).user.id; final isMyMessage = message.user.id == userId; final isNextUser = - index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id; + index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id; final channel = StreamChannel.of(context).channel; final readList = channel.state?.read @@ -673,24 +774,12 @@ class _MessageListViewState extends State { _messageNewListener = streamChannel.channel.on(EventType.messageNew).listen((event) { - final firstElementInViewport = - _itemPositionListener.itemPositions.value.first; if (event.message.user.id == streamChannel.channel.client.state.user.id) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo( index: 0, ); }); - } else { - if (firstElementInViewport.index != 0) { - _scrollController.jumpTo( - index: firstElementInViewport.index + 1, - alignment: firstElementInViewport.itemLeadingEdge, - ); - } - } - if (firstElementInViewport.index == 0) { - streamChannel.channel.markRead(); } }); diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 5dbfd103..0ca87b96 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -10,19 +10,23 @@ enum QueryDirection { top, bottom } /// /// Use [StreamChannel.of] to get the current [StreamChannelState] instance. class StreamChannel extends StatefulWidget { - StreamChannel({ + const StreamChannel({ Key key, @required this.child, @required this.channel, this.showLoading = true, - }) : super( - key: key, - ); + this.initialMessageId, + }) : assert(child != null), + assert(channel != null), + super(key: key); final Widget child; final Channel channel; final bool showLoading; + /// If passed the channel will load from this particular message. + final String initialMessageId; + /// Use this method to get the current [StreamChannelState] instance static StreamChannelState of(BuildContext context) { StreamChannelState streamChannelState; @@ -45,135 +49,123 @@ class StreamChannelState extends State { /// Current channel Channel get channel => widget.channel; + /// InitialMessageId + String get initialMessageId => widget.initialMessageId; + /// Current channel state stream Stream get channelStateStream => widget.channel.state.channelStateStream; - final BehaviorSubject _queryMessageController = BehaviorSubject(); + final _queryTopMessagesController = BehaviorSubject.seeded(false); + final _queryBottomMessagesController = BehaviorSubject.seeded(false); - /// The stream notifying the state of queryMessage call - Stream get queryMessage => _queryMessageController.stream; + /// The stream notifying the state of [_queryTopMessages] call + Stream get queryTopMessages => _queryTopMessagesController.stream; + + /// The stream notifying the state of [_queryBottomMessages] call + Stream get queryBottomMessages => _queryBottomMessagesController.stream; bool _topPaginationEnded = false; bool _bottomPaginationEnded = false; - /// Calls [channel.query] updating [queryMessage] stream - void queryMessages({QueryDirection direction = QueryDirection.top}) { - if (_queryMessageController.value == true || - (_topPaginationEnded && _bottomPaginationEnded)) { + Future _queryTopMessages({ + int limit = 20, + bool preferOffline = false, + }) async { + if (_topPaginationEnded || _queryTopMessagesController?.value == true) { return; } + _queryTopMessagesController.add(true); - _queryMessageController.add(true); - - String id; - PaginationParams params; - - final messageLimit = 25; - - if (channel.state.messages.isNotEmpty) { - switch (direction) { - case QueryDirection.top: - id = channel.state.messages.first.id; - params = PaginationParams( - lessThan: id, - limit: messageLimit, - ); - break; - case QueryDirection.bottom: - id = channel.state.messages.last.id; - params = PaginationParams( - greaterThan: id, - limit: messageLimit, - ); - break; - } + if (channel.state.messages.isEmpty) { + return _queryTopMessagesController.add(false); } - widget.channel - .query( - messagesPagination: params, - preferOffline: true, - ) - .then((res) { - if (res.messages.isEmpty || res.messages.length < messageLimit) { - switch (direction) { - case QueryDirection.top: - _topPaginationEnded = true; - break; - case QueryDirection.bottom: - _bottomPaginationEnded = true; - break; - } + final oldestMessage = channel.state.messages.first; + + try { + final state = await queryBeforeMessage( + oldestMessage.id, + limit: limit, + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + _topPaginationEnded = true; } - _queryMessageController.add(false); - }).catchError((e, stack) { - if (!_queryMessageController.isClosed) { - _queryMessageController.addError(e, stack); + _queryTopMessagesController.add(false); + } catch (e, stk) { + _queryTopMessagesController.addError(e, stk); + } + } + + Future _queryBottomMessages({ + int limit = 20, + bool preferOffline = false, + }) async { + if (_bottomPaginationEnded || + _queryBottomMessagesController?.value == true || + channel?.state?.isUpToDate == true) return; + _queryBottomMessagesController.add(true); + + if (channel.state.messages.isEmpty) { + return _queryBottomMessagesController.add(false); + } + + final recentMessage = channel.state.messages.last; + + try { + final state = await queryAfterMessage( + recentMessage.id, + limit: limit, + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + _bottomPaginationEnded = true; } - }); + _queryBottomMessagesController.add(false); + } catch (e, stk) { + _queryBottomMessagesController.addError(e, stk); + } + } + + /// Calls [channel.query] updating [queryMessage] stream + Future queryMessages({QueryDirection direction = QueryDirection.top}) { + if (direction == QueryDirection.top) return _queryTopMessages(); + return _queryBottomMessages(); } /// Calls [channel.getReplies] updating [queryMessage] stream Future getReplies( String parentId, { - QueryDirection direction = QueryDirection.top, + int limit = 50, + bool preferOffline = false, }) async { - if (_queryMessageController.value == true || - (_topPaginationEnded && _bottomPaginationEnded)) { - return; + if (_topPaginationEnded || _queryTopMessagesController.value) return; + _queryTopMessagesController.add(true); + + if (!channel.state.threads.containsKey(parentId)) { + return _queryTopMessagesController.add(false); } - _queryMessageController.add(true); + final thread = channel.state.threads[parentId]; - String id; - PaginationParams params; + if (thread.isEmpty) return _queryTopMessagesController.add(false); - final messageLimit = 50; + final message = thread.first; - if (widget.channel.state.threads.containsKey(parentId)) { - final thread = widget.channel.state.threads[parentId]; - if (thread != null && thread.isNotEmpty) { - switch (direction) { - case QueryDirection.top: - id = thread?.first?.id; - params = PaginationParams( - lessThan: id, - limit: messageLimit, - ); - break; - case QueryDirection.bottom: - id = thread?.last?.id; - params = PaginationParams( - greaterThan: id, - limit: messageLimit, - ); - break; - } + try { + final state = await queryBeforeMessage( + message.id, + limit: limit, + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + _topPaginationEnded = true; } + _queryTopMessagesController.add(false); + } catch (e, stk) { + _queryTopMessagesController.addError(e, stk); } - - return widget.channel - .getReplies( - parentId, - params, - preferOffline: true, - ) - .then((res) { - if (res.messages.isEmpty || res.messages.length < messageLimit) { - switch (direction) { - case QueryDirection.top: - _topPaginationEnded = true; - break; - case QueryDirection.bottom: - _bottomPaginationEnded = true; - break; - } - } - _queryMessageController.add(false); - }).catchError((e, stack) { - _queryMessageController.addError(e, stack); - }); } /// Query the channel members and watchers @@ -190,41 +182,148 @@ class StreamChannelState extends State { ); } + /// Loads channel at specific message + Future loadChannelAtMessage( + String messageId, { + int before = 20, + int after = 20, + bool preferOffline = false, + }) { + return queryAtMessage( + messageId: messageId, + before: before, + after: after, + preferOffline: preferOffline, + ); + } + + /// + Future queryAtMessage({ + String messageId, + int before = 20, + int after = 20, + bool preferOffline = false, + }) async { + if (channel.state == null) return; + channel.state.isUpToDate = false; + channel.state.truncate(); + + if (messageId == null) { + await channel.query( + messagesPagination: PaginationParams( + limit: before, + ), + preferOffline: preferOffline, + ); + channel.state.isUpToDate = true; + return; + } + + return Future.wait([ + queryBeforeMessage( + messageId, + limit: before, + preferOffline: preferOffline, + ), + queryAfterMessage( + messageId, + limit: after, + preferOffline: preferOffline, + ), + ]); + } + + /// + Future queryBeforeMessage( + String messageId, { + int limit = 20, + bool preferOffline = false, + }) { + return channel.query( + messagesPagination: PaginationParams( + lessThan: messageId, + limit: limit, + ), + preferOffline: preferOffline, + ); + } + + /// + Future queryAfterMessage( + String messageId, { + int limit = 20, + bool preferOffline = false, + }) async { + final state = await channel.query( + messagesPagination: PaginationParams( + greaterThanOrEqual: messageId, + limit: limit, + ), + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + channel.state.isUpToDate = true; + } + return state; + } + + /// Reloads the channel with latest message + Future reloadChannel() => queryAtMessage(before: 30); + + List> _futures; + + Future get _loadChannelAtMessage async { + try { + await loadChannelAtMessage(initialMessageId); + return true; + } catch (e, stk) { + print('Error: $e\nStack: $stk'); + rethrow; + } + } + + @override + void initState() { + super.initState(); + _futures = [widget.channel.initialized]; + if (initialMessageId != null) { + _futures.add(_loadChannelAtMessage); + } + } + @override void dispose() { - _queryMessageController.close(); + _queryTopMessagesController.close(); + _queryBottomMessagesController.close(); super.dispose(); } @override Widget build(BuildContext context) { - if (widget.channel == null) { - return Center( - child: CircularProgressIndicator(), - ); - } - return FutureBuilder( - future: widget.channel.initialized, - initialData: widget.channel.state != null, + Widget child = FutureBuilder>( + future: Future.wait(_futures), + initialData: [ + channel.state != null, + if (initialMessageId != null) false, + ], builder: (context, snapshot) { - if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) { - return Container( - height: 30, - child: Center( - child: CircularProgressIndicator(), - ), + final initialized = snapshot.data[0]; + final dataLoaded = initialMessageId == null ? true : snapshot.data[1]; + if (widget.showLoading && (!initialized || !dataLoaded)) { + return Center( + child: CircularProgressIndicator(), ); } else if (snapshot.hasError) { - return Container( - height: 30, - child: Center( - child: Text(snapshot.error), - ), + return Center( + child: Text(snapshot.error), ); - } else { - return widget.child; } + return widget.child; }, ); + if (initialMessageId != null) { + child = Material(child: child); + } + return child; } } diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index b57e2d55..928ea39e 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -339,7 +339,9 @@ class _UserListViewState extends State ); return LazyLoadScrollView( - onEndOfPage: () => _listenUserPagination(usersBlocState), + onEndOfPage: () async { + return _listenUserPagination(usersBlocState); + }, child: child, ); }, diff --git a/pubspec.yaml b/pubspec.yaml index a226f9c3..55f2bc65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,9 +29,10 @@ dependencies: image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 stream_chat: - git: - url: https://github.com/GetStream/stream-chat-dart - ref: two-way-pagination + path: ../stream-chat-dart +# git: +# url: https://github.com/GetStream/stream-chat-dart +# ref: two-way-pagination mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 3f300bdfa020c7900cd06e771819a590e4c87afb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 16:57:43 +0530 Subject: [PATCH 08/24] ref git url in pubspec Signed-off-by: Sahil Kumar --- pubspec.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 55f2bc65..a226f9c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,10 +29,9 @@ dependencies: image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 stream_chat: - path: ../stream-chat-dart -# git: -# url: https://github.com/GetStream/stream-chat-dart -# ref: two-way-pagination + git: + url: https://github.com/GetStream/stream-chat-dart + ref: two-way-pagination mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 8b4ff142bab22c8f214a16a22a03c8de318f4fed Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 17:06:09 +0530 Subject: [PATCH 09/24] Merge conflicts Signed-off-by: Sahil Kumar --- lib/src/message_input.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 447d49d9..dd7d90ca 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -21,7 +21,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; -import 'package:photo_manager/photo_manager.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -462,6 +461,7 @@ class MessageInputState extends State { } Timer _debounce; + void _onChanged(BuildContext context, String s) { if (_debounce?.isActive == true) _debounce.cancel(); _debounce = Timer( @@ -1956,7 +1956,11 @@ class MessageInputState extends State { message = await widget.preMessageSending(message); } - final channel = StreamChannel.of(context).channel; + final streamChannel = StreamChannel.of(context); + final channel = streamChannel.channel; + if (!channel.state.isUpToDate) { + await streamChannel.reloadChannel(); + } if (widget.editMessage == null || widget.editMessage.status == MessageSendingStatus.FAILED) { @@ -2043,6 +2047,7 @@ class MessageInputState extends State { } bool _initialized = false; + @override void didChangeDependencies() { if (widget.editMessage != null && !_initialized) { From 12105a3120017dc02c7758d93e12e3b3be993fff Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 19:03:03 +0530 Subject: [PATCH 10/24] Merge fixes Signed-off-by: Sahil Kumar --- example/lib/new_chat_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index a2ce9ebf..54371c3b 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -358,7 +358,7 @@ class _NewChatScreenState extends State { context, Routes.CHANNEL_PAGE, ModalRoute.withName(Routes.HOME), - arguments: channel, + arguments: ChannelPageArgs(channel: channel), ); }, ), From 336580761510f39d472a67e27f7576f20d4ed44e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Dec 2020 15:45:07 +0100 Subject: [PATCH 11/24] fix unread count indicator --- lib/src/message_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 12adaf3e..4f40feb1 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -529,7 +529,7 @@ class _MessageListViewState extends State { child: Padding( padding: const EdgeInsets.all(3.0), child: Text( - snapshot.data.toString(), + '$unreadCount', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, From 4a165249a1918621cb07a8c394b35c80d638d80b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Dec 2020 15:47:35 +0100 Subject: [PATCH 12/24] revert api key --- example/lib/choose_user_page.dart | 2 +- example/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index 73b12425..dde87376 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -13,7 +13,7 @@ import 'routes/routes.dart'; const kStreamApiKey = 'STREAM_API_KEY'; const kStreamUserId = 'STREAM_USER_ID'; const kStreamToken = 'STREAM_TOKEN'; -const kDefaultStreamApiKey = 's2dxdhpxd94g'; +const kDefaultStreamApiKey = 'uj7qrdbfrzvg'; class ChooseUserPage extends StatelessWidget { @override diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f9c71355..ef06f719 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.91+94 +version: 1.0.92+95 environment: sdk: ">=2.2.2 <3.0.0" From 7fd98111d23dd14625bb52ad7301e3a7413212bd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Dec 2020 16:00:29 +0100 Subject: [PATCH 13/24] revert api key --- example/lib/choose_user_page.dart | 2 +- example/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index dde87376..a64013b9 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -19,7 +19,7 @@ class ChooseUserPage extends StatelessWidget { @override Widget build(BuildContext context) { final users = { - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg': + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI': User( id: 'vishal', extraData: { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ef06f719..cec1b886 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.92+95 +version: 1.0.93+96 environment: sdk: ">=2.2.2 <3.0.0" From e17a3c30b790ee493430a51527ed43de528de88a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Dec 2020 14:42:46 +0530 Subject: [PATCH 14/24] [LazyLoadScrollView] Fix callback logic Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 89 ++++++++++++++++-------------- lib/src/message_list_view.dart | 11 ++-- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index a3c344aa..99eeb861 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -15,7 +15,7 @@ class LazyLoadScrollView extends StatefulWidget { /// Called when the [child] reaches the end of the list final AsyncCallback onEndOfPage; - /// The offset to take into account when triggering [onEndOfPage] in pixels + /// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels final double scrollOffset; /// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder @@ -38,6 +38,7 @@ class LazyLoadScrollView extends StatefulWidget { class _LazyLoadScrollViewState extends State { _LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE; + double _scrollPosition = 0.0; @override Widget build(BuildContext context) { @@ -49,59 +50,65 @@ class _LazyLoadScrollViewState extends State { bool _onNotification(Notification notification) { if (notification is ScrollUpdateNotification) { - if (notification.metrics.maxScrollExtent > notification.metrics.pixels && - notification.metrics.maxScrollExtent - notification.metrics.pixels <= - widget.scrollOffset) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onEndOfPage != null) { - widget.onEndOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); - } + final pixels = notification.metrics.pixels; + final extentBefore = notification.metrics.extentBefore; + final extentAfter = notification.metrics.extentAfter; + final scrollOffset = widget.scrollOffset; + + final scrollingDown = _scrollPosition < pixels; + + if (scrollOffset == null || scrollOffset == 0) { + if (extentAfter == 0) { + _onEndOfPage(); + } + if (extentBefore == 0) { + _onStartOfPage(); } - } - if (notification.metrics.minScrollExtent < notification.metrics.pixels && - notification.metrics.pixels - notification.metrics.minScrollExtent <= - widget.scrollOffset) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onStartOfPage != null) { - widget.onStartOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); + } else { + if (scrollingDown) { + if (extentAfter <= scrollOffset) { + _onEndOfPage(); + } + } else { + if (extentBefore <= scrollOffset) { + _onStartOfPage(); } } } + _scrollPosition = pixels; return true; } if (notification is OverscrollNotification) { if (notification.overscroll > 0) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onEndOfPage != null) { - widget.onEndOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); - } - } + _onEndOfPage(); } if (notification.overscroll < 0) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onStartOfPage != null) { - widget.onStartOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); - } - } + _onStartOfPage(); } return true; } return false; } + + void _onEndOfPage() { + if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + if (widget.onEndOfPage != null) { + widget.onEndOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } + } + } + + void _onStartOfPage() { + if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + if (widget.onStartOfPage != null) { + widget.onStartOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } + } + } } diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 4f40feb1..d8a9d416 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -294,13 +294,13 @@ class _MessageListViewState extends State { if (!_upToDate) { _topPaginationActive = false; _bottomPaginationActive = true; - _paginateData(streamChannel, QueryDirection.bottom); + return _paginateData(streamChannel, QueryDirection.bottom); } }, onEndOfPage: () async { _topPaginationActive = true; _bottomPaginationActive = false; - _paginateData(streamChannel, QueryDirection.top); + return _paginateData(streamChannel, QueryDirection.top); }, child: ScrollablePositionedList.builder( key: ValueKey(initialIndex + initialAlignment), @@ -453,11 +453,12 @@ class _MessageListViewState extends State { }); } - void _paginateData(StreamChannelState channel, QueryDirection direction) { + Future _paginateData( + StreamChannelState channel, QueryDirection direction) { if (widget.parentMessage == null) { - channel.queryMessages(direction: direction); + return channel.queryMessages(direction: direction); } else { - channel.getReplies(widget.parentMessage.id); + return channel.getReplies(widget.parentMessage.id); } } From 84aa3fbb87df9acb858fa52aabbddcbfc5b84d2d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Dec 2020 17:10:45 +0530 Subject: [PATCH 15/24] Use localChannel if present instead of creating a new Signed-off-by: Sahil Kumar --- example/lib/main.dart | 7 +- lib/src/message_list_view.dart | 396 +++++++++++++++++---------------- lib/src/stream_channel.dart | 21 +- 3 files changed, 225 insertions(+), 199 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ca7006b0..1c61dde2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -335,10 +335,11 @@ class _ChannelListPageState extends State { onItemTap: (messageResponse) async { final client = StreamChat.of(context).client; final message = messageResponse.message; - final channel = Channel.fromState( - client, - ChannelState(channel: messageResponse.channel), + final channel = client.channel( + messageResponse.channel.type, + id: messageResponse.channel.id, ); + await channel.watch(); Navigator.pushNamed( context, Routes.CHANNEL_PAGE, diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index d8a9d416..ca184d6b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -183,7 +183,7 @@ class _MessageListViewState extends State { final messageIndex = messages.indexWhere((e) { return e.id == streamChannel.initialMessageId; }); - return totalMessages - messageIndex - 1; + return totalMessages - messageIndex; } return 0; } @@ -234,223 +234,235 @@ class _MessageListViewState extends State { initialAlignment = _initialAlignment; } - return StreamBuilder>( - stream: messagesStream?.map((messages) => messages - ?.where((e) => - !e.isDeleted || - (e.isDeleted && - e.user.id == streamChannel.channel.client.state.user.id)) - ?.toList()), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - final messageList = snapshot.data?.reversed?.toList() ?? []; - - if (messageList.isEmpty) { - if (_upToDate) { + return WillPopScope( + onWillPop: () async { + if (!_upToDate) { + await streamChannel.reloadChannel(); + } + return true; + }, + child: StreamBuilder>( + stream: messagesStream?.map((messages) => messages + ?.where((e) => + !e.isDeleted || + (e.isDeleted && + e.user.id == streamChannel.channel.client.state.user.id)) + ?.toList()), + builder: (context, snapshot) { + if (!snapshot.hasData) { return Center( - child: Text( - 'No chats here yet...', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(.5), - ), - ), + child: CircularProgressIndicator(), ); } - } else { - messages = messageList; - } - final newMessagesListLength = messages.length; + final messageList = snapshot.data?.reversed?.toList() ?? []; - if (_bottomPaginationActive) { - if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && - _messageListLength != null) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - initialIndex = first.index + diff; - initialAlignment = first.itemLeadingEdge; + if (messageList.isEmpty) { + if (_upToDate) { + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), + ), + ); } + } else { + messages = messageList; } - } else if (!_topPaginationActive && _upToDate) { - // Reset the index in-case we send any new message - initialIndex = 0; - initialAlignment = 0; - } - _messageListLength = newMessagesListLength; + final newMessagesListLength = messages.length; - return Stack( - alignment: Alignment.center, - children: [ - LazyLoadScrollView( - onStartOfPage: () async { - if (!_upToDate) { - _topPaginationActive = false; - _bottomPaginationActive = true; - return _paginateData(streamChannel, QueryDirection.bottom); - } - }, - onEndOfPage: () async { - _topPaginationActive = true; - _bottomPaginationActive = false; - return _paginateData(streamChannel, QueryDirection.top); - }, - child: ScrollablePositionedList.builder( - key: ValueKey(initialIndex + initialAlignment), - itemPositionsListener: _itemPositionListener, - addAutomaticKeepAlives: true, - initialScrollIndex: initialIndex ?? 0, - initialAlignment: initialAlignment ?? 0, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - itemCount: messages.length + - 2 + - (widget.parentMessage != null ? 1 : 0), - itemBuilder: (context, i) { - if (i == messages.length + 2) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( + if (_bottomPaginationActive) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == + true && + _messageListLength != null) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; + } + } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; + } + + _messageListLength = newMessagesListLength; + + return Stack( + alignment: Alignment.center, + children: [ + LazyLoadScrollView( + onStartOfPage: () async { + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; + return _paginateData( + streamChannel, QueryDirection.bottom); + } + }, + onEndOfPage: () async { + _topPaginationActive = true; + _bottomPaginationActive = false; + return _paginateData(streamChannel, QueryDirection.top); + }, + child: ScrollablePositionedList.builder( + key: ValueKey(initialIndex + initialAlignment), + itemPositionsListener: _itemPositionListener, + addAutomaticKeepAlives: true, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + itemCount: messages.length + + 2 + + (widget.parentMessage != null ? 1 : 0), + itemBuilder: (context, i) { + if (i == messages.length + 2) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, + widget.parentMessage, + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildParentMessage(widget.parentMessage), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Start of thread', + textAlign: TextAlign.center, + ), + color: Theme.of(context) + .accentColor + .withAlpha(50), + ), + ), + ], + ); + } + } + if (i == messages.length + 1) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.top, + ); + } + if (i == 0) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.bottom, + ); + } + final message = messages[i - 1]; + final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; + + Widget messageWidget; + + if (i == 1) { + messageWidget = _buildBottomMessage( context, - widget.parentMessage, + message, + messages, + streamChannel, + ); + } else if (i == messages.length - 1) { + messageWidget = _buildTopMessage( + context, + message, + messages, + streamChannel, ); } else { + if (widget.messageBuilder != null) { + messageWidget = Builder( + key: ValueKey('MESSAGE-${message.id}'), + builder: (_) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + messages, + i, + ), + messages), + ); + } else { + messageWidget = buildMessage(message, messages, i); + } + } + + if (nextMessage != null && + !Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), Units.DAY)) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - buildParentMessage(widget.parentMessage), + messageWidget, Padding( padding: - const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - 'Start of thread', - textAlign: TextAlign.center, - ), - color: - Theme.of(context).accentColor.withAlpha(50), - ), + const EdgeInsets.symmetric(vertical: 12.0), + child: widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal()) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ), ), ], ); } - } - if (i == messages.length + 1) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.top, - ); - } - if (i == 0) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.bottom, - ); - } - final message = messages[i - 1]; - final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; - Widget messageWidget; - - if (i == 1) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel, - ); - } else if (i == messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - messages, - streamChannel, - ); - } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = buildMessage(message, messages, i); + return messageWidget; + }, + ), + ), + if (widget.showScrollToBottom) _buildScrollToBottom(), + Positioned( + top: 20.0, + child: ValueListenableBuilder>( + valueListenable: _itemPositionListener.itemPositions, + builder: (context, values, _) { + final items = _itemPositionListener.itemPositions?.value; + if (items.isEmpty || messages.isEmpty) { + return SizedBox(); } - } - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), Units.DAY)) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - messageWidget, - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ), - ], - ); - } + var index = _getTopElement(values).index; - return messageWidget; - }, + if (index > messages.length) { + return SizedBox(); + } + + if (index == messages.length) { + index = max(index - 1, 0); + } + + return widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + messages[index].createdAt.toLocal(), + ) + : DateDivider( + dateTime: messages[index].createdAt.toLocal(), + ); + }, + ), ), - ), - if (widget.showScrollToBottom) _buildScrollToBottom(), - Positioned( - top: 20.0, - child: ValueListenableBuilder>( - valueListenable: _itemPositionListener.itemPositions, - builder: (context, values, _) { - final items = _itemPositionListener.itemPositions?.value; - if (items.isEmpty || messages.isEmpty) { - return SizedBox(); - } - - var index = _getTopElement(values).index; - - if (index > messages.length) { - return SizedBox(); - } - - if (index == messages.length) { - index = max(index - 1, 0); - } - - return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - messages[index].createdAt.toLocal(), - ) - : DateDivider( - dateTime: messages[index].createdAt.toLocal(), - ); - }, - ), - ), - ], - ); - }); + ], + ); + }), + ); } Future _paginateData( diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 0ca87b96..001d98ff 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -307,16 +307,29 @@ class StreamChannelState extends State { if (initialMessageId != null) false, ], builder: (context, snapshot) { + if (snapshot.hasError) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + message = 'Check your connection and retry'; + } + } + return Center( + child: Text(message), + ); + } final initialized = snapshot.data[0]; final dataLoaded = initialMessageId == null ? true : snapshot.data[1]; if (widget.showLoading && (!initialized || !dataLoaded)) { return Center( child: CircularProgressIndicator(), ); - } else if (snapshot.hasError) { - return Center( - child: Text(snapshot.error), - ); } return widget.child; }, From 49377b46a21def9076b6de5cd1caee78de2014cf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Dec 2020 23:15:56 +0530 Subject: [PATCH 16/24] fix initialIndex bug Signed-off-by: Sahil Kumar --- example/lib/main.dart | 4 +- lib/src/message_list_view.dart | 78 ++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 1c61dde2..9f41efd5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -339,7 +339,9 @@ class _ChannelListPageState extends State { messageResponse.channel.type, id: messageResponse.channel.id, ); - await channel.watch(); + if (channel.state == null) { + await channel.watch(); + } Navigator.pushNamed( context, Routes.CHANNEL_PAGE, diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index ca184d6b..254af757 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -183,22 +183,15 @@ class _MessageListViewState extends State { final messageIndex = messages.indexWhere((e) { return e.id == streamChannel.initialMessageId; }); - return totalMessages - messageIndex; + final index = totalMessages - messageIndex; + if (index != 0) return index - 1; + return index; } return 0; } double get _initialAlignment { if (widget.initialAlignment != null) return widget.initialAlignment; - final streamChannel = StreamChannel.of(context); - if (streamChannel.initialMessageId != null) { - final messages = streamChannel.channel.state.messages; - final messageIndex = messages.indexWhere((e) { - return e.id == streamChannel.initialMessageId; - }); - final isFirstMessage = messageIndex == 0; - return isFirstMessage ? 0 : 0.5; - } return 0; } @@ -212,13 +205,13 @@ class _MessageListViewState extends State { bool _topPaginationActive = false; bool _bottomPaginationActive = false; - bool get _paginationActive => _topPaginationActive || _bottomPaginationActive; - int initialIndex; double initialAlignment; List messages = []; + bool initialMessageHighlightComplete = false; + @override Widget build(BuildContext context) { final streamChannel = StreamChannel.of(context); @@ -229,11 +222,6 @@ class _MessageListViewState extends State { .map((threads) => threads[widget.parentMessage.id]) : streamChannel.channel.state?.messagesStream; - if (!_paginationActive && !_upToDate) { - initialIndex = _initialIndex; - initialAlignment = _initialAlignment; - } - return WillPopScope( onWillPop: () async { if (!_upToDate) { @@ -275,21 +263,22 @@ class _MessageListViewState extends State { final newMessagesListLength = messages.length; - if (_bottomPaginationActive) { - if (_itemPositionListener.itemPositions.value?.isNotEmpty == - true && - _messageListLength != null) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - initialIndex = first.index + diff; - initialAlignment = first.itemLeadingEdge; + if (_messageListLength != null) { + if (_bottomPaginationActive) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == + true) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; + } } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; } - } else if (!_topPaginationActive && _upToDate) { - // Reset the index in-case we send any new message - initialIndex = 0; - initialAlignment = 0; } _messageListLength = newMessagesListLength; @@ -731,7 +720,7 @@ class _MessageListViewState extends State { final allRead = readList.length >= (channel.memberCount ?? 0) - 1; - return MessageWidget( + Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, reverse: isMyMessage, @@ -773,6 +762,30 @@ class _MessageListViewState extends State { readList: readList, allRead: allRead, ); + + if (!initialMessageHighlightComplete && + widget.highlightInitialMessage && + _isInitialMessage(message.id)) { + final accentColor = Theme.of(context).accentColor; + child = TweenAnimationBuilder( + tween: ColorTween( + begin: accentColor.withOpacity(0.7), + end: Colors.transparent, + ), + duration: const Duration(seconds: 2), + child: child, + onEnd: () { + initialMessageHighlightComplete = true; + }, + builder: (_, color, child) { + return Container( + color: color, + child: child, + ); + }, + ); + } + return child; } StreamSubscription _messageNewListener; @@ -785,6 +798,9 @@ class _MessageListViewState extends State { final streamChannel = StreamChannel.of(context); + initialIndex = _initialIndex; + initialAlignment = _initialAlignment; + _messageNewListener = streamChannel.channel.on(EventType.messageNew).listen((event) { if (event.message.user.id == streamChannel.channel.client.state.user.id) { From 8ee4b585b8d80e3096e1f3c7b68d7f8c706136f4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 10:00:10 +0100 Subject: [PATCH 17/24] mark read only if the channel is up to date --- lib/src/message_list_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 254af757..6d1e61f2 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -511,6 +511,7 @@ class _MessageListViewState extends State { _bottomPaginationActive = false; _topPaginationActive = false; streamChannel.reloadChannel(); + streamChannel.channel.markRead(); } else { setState(() => _showScrollToBottom = false); _scrollController.scrollTo( @@ -642,7 +643,8 @@ class _MessageListViewState extends State { onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; if (isVisible && !_bottomWasVisible) { - if (streamChannel.channel.config?.readEvents == true && + if (streamChannel.channel.state.isUpToDate && + streamChannel.channel.config?.readEvents == true && streamChannel.channel.state.unreadCount > 0) { streamChannel.channel.markRead(); } From 7cfb1752fba709ba808f3c7361024dc9f2aca961 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Dec 2020 17:11:46 +0530 Subject: [PATCH 18/24] fix newMessage jump bug Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 38 ++++++++++++++++++++++++++++-- lib/src/message_list_view.dart | 22 ++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index 99eeb861..2bc82a55 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -15,6 +15,15 @@ class LazyLoadScrollView extends StatefulWidget { /// Called when the [child] reaches the end of the list final AsyncCallback onEndOfPage; + /// Called when the list scrolling starts + final VoidCallback onPageScrollStart; + + /// Called when the list scrolling ends + final VoidCallback onPageScrollEnd; + + /// Called every time the [child] is in-between the list + final VoidCallback onInBetweenOfPage; + /// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels final double scrollOffset; @@ -27,6 +36,9 @@ class LazyLoadScrollView extends StatefulWidget { @required this.child, this.onStartOfPage, this.onEndOfPage, + this.onPageScrollStart, + this.onPageScrollEnd, + this.onInBetweenOfPage, this.isLoading = false, this.scrollOffset = 100, }) : assert(child != null), @@ -49,12 +61,34 @@ class _LazyLoadScrollViewState extends State { } bool _onNotification(Notification notification) { + if (notification is ScrollStartNotification) { + if (widget.onPageScrollStart != null) { + widget.onPageScrollStart(); + return true; + } + } + if (notification is ScrollEndNotification) { + if (widget.onPageScrollEnd != null) { + widget.onPageScrollEnd(); + return true; + } + } if (notification is ScrollUpdateNotification) { final pixels = notification.metrics.pixels; - final extentBefore = notification.metrics.extentBefore; - final extentAfter = notification.metrics.extentAfter; + final maxScrollExtent = notification.metrics.maxScrollExtent; + final minScrollExtent = notification.metrics.minScrollExtent; final scrollOffset = widget.scrollOffset; + if (pixels > (minScrollExtent + scrollOffset) && + pixels < (maxScrollExtent - scrollOffset)) { + if (widget.onInBetweenOfPage != null) { + widget.onInBetweenOfPage(); + return true; + } + } + + final extentBefore = notification.metrics.extentBefore; + final extentAfter = notification.metrics.extentAfter; final scrollingDown = _scrollPosition < pixels; if (scrollOffset == null || scrollOffset == 0) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 254af757..f4ab3103 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -212,6 +212,8 @@ class _MessageListViewState extends State { bool initialMessageHighlightComplete = false; + bool _inBetweenList = false; + @override Widget build(BuildContext context) { final streamChannel = StreamChannel.of(context); @@ -264,7 +266,7 @@ class _MessageListViewState extends State { final newMessagesListLength = messages.length; if (_messageListLength != null) { - if (_bottomPaginationActive) { + if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { if (_itemPositionListener.itemPositions.value?.isNotEmpty == true) { final first = _itemPositionListener.itemPositions.value.first; @@ -288,17 +290,27 @@ class _MessageListViewState extends State { children: [ LazyLoadScrollView( onStartOfPage: () async { + _inBetweenList = false; if (!_upToDate) { _topPaginationActive = false; _bottomPaginationActive = true; return _paginateData( - streamChannel, QueryDirection.bottom); + streamChannel, + QueryDirection.bottom, + ); } }, onEndOfPage: () async { + _inBetweenList = false; _topPaginationActive = true; _bottomPaginationActive = false; - return _paginateData(streamChannel, QueryDirection.top); + return _paginateData( + streamChannel, + QueryDirection.top, + ); + }, + onInBetweenOfPage: () { + _inBetweenList = true; }, child: ScrollablePositionedList.builder( key: ValueKey(initialIndex + initialAlignment), @@ -803,6 +815,10 @@ class _MessageListViewState extends State { _messageNewListener = streamChannel.channel.on(EventType.messageNew).listen((event) { + if (_upToDate) { + _bottomPaginationActive = false; + _topPaginationActive = false; + } if (event.message.user.id == streamChannel.channel.client.state.user.id) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo( From 9e3248d4c9132cb52ec8e590185d444100b4675f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Dec 2020 17:13:55 +0530 Subject: [PATCH 19/24] minor refactor Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 56d246bf..b56add20 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -655,9 +655,10 @@ class _MessageListViewState extends State { onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; if (isVisible && !_bottomWasVisible) { - if (streamChannel.channel.state.isUpToDate && - streamChannel.channel.config?.readEvents == true && - streamChannel.channel.state.unreadCount > 0) { + final channel = streamChannel.channel; + if (_upToDate && + channel.config?.readEvents == true && + channel.state.unreadCount > 0) { streamChannel.channel.markRead(); } _bottomWasVisible = !isVisible; From 966548132304084135e7f2d2c320be4097eb61bb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Dec 2020 19:55:27 +0530 Subject: [PATCH 20/24] Change cupertinoActivitYIndicator to cicularProgressIndicator Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index b56add20..e2807775 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -592,7 +592,7 @@ class _MessageListViewState extends State { return Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: const CupertinoActivityIndicator(), + child: const CircularProgressIndicator(), ), ); }); From 14aebadabf1edc2676b364cd8f4bea7cf762bafa Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 15:42:31 +0100 Subject: [PATCH 21/24] fix markread --- example/ios/Flutter/.last_build_id | 2 +- example/ios/Podfile.lock | 4 ++-- example/lib/main.dart | 2 +- example/pubspec.yaml | 2 +- lib/src/message_list_view.dart | 4 +++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index e72273c3..f57e1e1f 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -13a41b8138c4868054e44b5158f3bdd6 \ No newline at end of file +c770358a10272b4ee1cdc2eb432445f6 \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index bf8c0e1a..f266b656 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -40,7 +40,7 @@ PODS: - Firebase/Messaging (6.33.0): - Firebase/CoreOnly - FirebaseMessaging (~> 4.7.0) - - firebase_core (0.5.2-1): + - firebase_core (0.5.3): - Firebase/CoreOnly (~> 6.33.0) - Flutter - firebase_messaging (7.0.3): @@ -258,7 +258,7 @@ SPEC CHECKSUMS: esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 - firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781 + firebase_core: 5d6a02f3d85acd5f8321c2d6d62877626a670659 firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75 FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 diff --git a/example/lib/main.dart b/example/lib/main.dart index c8bb34f5..f0f1e59b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,7 +24,7 @@ void main() async { final client = Client( apiKey ?? kDefaultStreamApiKey, - logLevel: Level.SEVERE, + logLevel: Level.INFO, showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, persistenceEnabled: true, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ef06f719..cec1b886 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.92+95 +version: 1.0.93+96 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index e2807775..df99687b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -519,11 +519,13 @@ class _MessageListViewState extends State { color: Colors.black, ), onPressed: () { + if (unreadCount > 0) { + streamChannel.channel.markRead(); + } if (!_upToDate) { _bottomPaginationActive = false; _topPaginationActive = false; streamChannel.reloadChannel(); - streamChannel.channel.markRead(); } else { setState(() => _showScrollToBottom = false); _scrollController.scrollTo( From e3b268fb4552cf3901384782f825f84c5523bfb9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 15:53:14 +0100 Subject: [PATCH 22/24] fix padding --- lib/src/message_list_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index df99687b..5a850ad3 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -745,7 +745,8 @@ class _MessageListViewState extends State { padding: EdgeInsets.only( left: 8.0, right: 8.0, - bottom: index == 0 ? 30 : (isNextUser ? 5 : 10), + bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), + top: 3, ), showUsername: !isMyMessage && !isNextUser, showSendingIndicator: isMyMessage && From 74781fd24f6da2fc0f9868a6587394ccb2b5cc08 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 15:54:13 +0100 Subject: [PATCH 23/24] fix message search date --- lib/src/message_search_item.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/message_search_item.dart b/lib/src/message_search_item.dart index 3e786b9a..35b31aa8 100644 --- a/lib/src/message_search_item.dart +++ b/lib/src/message_search_item.dart @@ -77,16 +77,16 @@ class MessageSearchItem extends StatelessWidget { } Widget _buildDate(BuildContext context, Message message) { - final lastUpdatedAt = message.updatedAt; + final createdAt = message.createdAt; String stringDate; final now = DateTime.now(); - if (now.year != lastUpdatedAt.year || - now.month != lastUpdatedAt.month || - now.day != lastUpdatedAt.day) { - stringDate = Jiffy(lastUpdatedAt.toLocal()).format('dd/MM/yyyy'); + if (now.year != createdAt.year || + now.month != createdAt.month || + now.day != createdAt.day) { + stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy'); } else { - stringDate = Jiffy(lastUpdatedAt.toLocal()).format('HH:mm'); + stringDate = Jiffy(createdAt.toLocal()).format('HH:mm'); } return Text( From abf218595e3d31d8220d72784d43ddba0b26b27f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Dec 2020 11:43:15 +0100 Subject: [PATCH 24/24] update llc dependency --- example/ios/Flutter/.last_build_id | 2 +- example/lib/new_chat_screen.dart | 2 +- example/pubspec.yaml | 2 +- lib/src/channels_bloc.dart | 10 +++++----- lib/src/chat_info_screen.dart | 4 ++-- pubspec.yaml | 5 +---- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index f57e1e1f..e72273c3 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -c770358a10272b4ee1cdc2eb432445f6 \ No newline at end of file +13a41b8138c4868054e44b5158f3bdd6 \ No newline at end of file diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 54371c3b..f29b1483 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -84,7 +84,7 @@ class _NewChatScreenState extends State { paginationParams: PaginationParams( limit: 1, ), - ).first; + ); final _channelExisted = res.length == 1; if (_channelExisted) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cec1b886..175cdaeb 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.93+96 +version: 1.0.94+97 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart index 474d9599..15167317 100644 --- a/lib/src/channels_bloc.dart +++ b/lib/src/channels_bloc.dart @@ -85,7 +85,7 @@ class ChannelsBlocState extends State paginationParams.offset == null || paginationParams.offset == 0; final oldChannels = List.from(channels ?? []); - client + await client .queryChannels( filter: filter, sort: sortOptions, @@ -93,19 +93,19 @@ class ChannelsBlocState extends State paginationParams: paginationParams, onlyOffline: onlyOffline, ) - .listen((channels) { + .then((channels) { if (clear) { _channelsController.add(channels); } else { final l = oldChannels + channels; _channelsController.add(l); } - }, onDone: () { - _queryChannelsLoadingController.sink.add(false); - }, onError: (err, stackTrace) { + }).catchError((err, stackTrace) { print(err); print(stackTrace); _queryChannelsLoadingController.addError(err, stackTrace); + }).whenComplete(() { + _queryChannelsLoadingController.sink.add(false); }); } catch (err, stackTrace) { _queryChannelsLoadingController.addError(err, stackTrace); diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 0ec62c36..8d291f38 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -391,8 +391,8 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ), backgroundColor: StreamChatTheme.of(context).primaryColor, ), - body: StreamBuilder>( - stream: chat.client.queryChannels( + body: FutureBuilder>( + future: chat.client.queryChannels( filter: { r'$and': [ { diff --git a/pubspec.yaml b/pubspec.yaml index a226f9c3..12374291 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,10 +28,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: - git: - url: https://github.com/GetStream/stream-chat-dart - ref: two-way-pagination + stream_chat: ^0.2.20 mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5