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