From 390062f1c20879183578be66a59a7f6ce5521cdb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 2 Dec 2020 19:30:24 +0530 Subject: [PATCH] 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