feat: 2 way pagination

Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2020-12-02 19:30:24 +05:30
parent ac0fc427ae
commit 390062f1c2
5 changed files with 219 additions and 141 deletions
+122 -108
View File
@@ -294,130 +294,136 @@ class _ChannelListPageState extends State<ChannelListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = StreamChat.of(context).user; final user = StreamChat.of(context).user;
return ChannelsBloc( return WillPopScope(
child: MessageSearchBloc( onWillPop: () async {
child: Column( if (_isSearchActive) {
children: [ _controller.clear();
SearchTextField( setState(() => _isSearchActive = false);
controller: _controller, return false;
showCloseButton: _isSearchActive, }
), return true;
Expanded( },
child: AnimatedSwitcher( child: ChannelsBloc(
duration: const Duration(milliseconds: 350), child: MessageSearchBloc(
child: GestureDetector( child: Column(
behavior: HitTestBehavior.opaque, children: [
onPanDown: (_) => FocusScope.of(context).unfocus(), SearchTextField(
child: _isSearchActive controller: _controller,
? MessageSearchListView( showCloseButton: _isSearchActive,
messageQuery: _channelQuery, ),
filters: { Expanded(
'members': { child: AnimatedSwitcher(
r'$in': [user.id] duration: const Duration(milliseconds: 350),
} child: GestureDetector(
}, behavior: HitTestBehavior.opaque,
sortOptions: [ onPanDown: (_) => FocusScope.of(context).unfocus(),
SortOption( child: _isSearchActive
'created_at', ? MessageSearchListView(
direction: SortOption.ASC, messageQuery: _channelQuery,
), filters: {
], 'members': {
paginationParams: PaginationParams(limit: 20), r'$in': [user.id]
onItemTap: (message) {}, }
)
: ChannelListView(
onStartChatPressed: () {
Navigator.pushNamed(context, Routes.NEW_CHAT);
},
swipeToAction: true,
filter: {
'members': {
r'$in': [user.id],
}, },
}, sortOptions: [
options: { SortOption(
'presence': true, 'created_at',
}, direction: SortOption.ASC,
pagination: PaginationParams( ),
limit: 20, ],
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 { class ChannelPageArgs {
final Stream<List<Message>> searchResultStream; final Channel channel;
final int initialScrollIndex;
final double initialAlignment;
const ChannelQuerySearchResultPage({ const ChannelPageArgs({
Key key, this.channel,
@required this.searchResultStream, this.initialScrollIndex = 0,
}) : super(key: key); this.initialAlignment = 0,
});
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Message>>(
initialData: const <Message>[],
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()),
);
},
),
),
],
);
},
);
}
} }
class ChannelPage extends StatelessWidget { class ChannelPage extends StatelessWidget {
final int initialScrollIndex;
final double initialAlignment;
const ChannelPage({ const ChannelPage({
Key key, Key key,
this.initialScrollIndex = 0,
this.initialAlignment = 0,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -433,6 +439,8 @@ class ChannelPage extends StatelessWidget {
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
MessageListView( MessageListView(
initialScrollIndex: initialScrollIndex,
initialAlignment: initialAlignment,
threadBuilder: (_, parentMessage) { threadBuilder: (_, parentMessage) {
return ThreadPage( return ThreadPage(
parent: parentMessage, parent: parentMessage,
@@ -467,10 +475,14 @@ class ChannelPage extends StatelessWidget {
class ThreadPage extends StatelessWidget { class ThreadPage extends StatelessWidget {
final Message parent; final Message parent;
final int initialScrollIndex;
final double initialAlignment;
ThreadPage({ ThreadPage({
Key key, Key key,
this.parent, this.parent,
this.initialScrollIndex = 0,
this.initialAlignment = 0,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -484,6 +496,8 @@ class ThreadPage extends StatelessWidget {
Expanded( Expanded(
child: MessageListView( child: MessageListView(
parentMessage: parent, parentMessage: parent,
initialScrollIndex: initialScrollIndex,
initialAlignment: initialAlignment,
), ),
), ),
if (parent.type != 'deleted') if (parent.type != 'deleted')
+6 -2
View File
@@ -34,9 +34,13 @@ class AppRoutes {
return MaterialPageRoute( return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHANNEL_PAGE), settings: const RouteSettings(name: Routes.CHANNEL_PAGE),
builder: (_) { builder: (_) {
final arg = args as ChannelPageArgs;
return StreamChannel( return StreamChannel(
channel: args as Channel, channel: arg.channel,
child: ChannelPage(), child: ChannelPage(
initialScrollIndex: arg.initialScrollIndex,
initialAlignment: arg.initialAlignment,
),
); );
}); });
case Routes.NEW_CHAT: case Routes.NEW_CHAT:
+13 -6
View File
@@ -480,8 +480,8 @@ class _MessageListViewState extends State<MessageListView> {
} else { } else {
streamChannel.getReplies(widget.parentMessage.id); streamChannel.getReplies(widget.parentMessage.id);
} }
_topWasVisible = !topIsVisible;
} }
_topWasVisible = topIsVisible;
}, },
); );
} }
@@ -515,14 +515,21 @@ class _MessageListViewState extends State<MessageListView> {
key: ValueKey<String>('BOTTOM-MESSAGE'), key: ValueKey<String>('BOTTOM-MESSAGE'),
onVisibilityChanged: (visibility) { onVisibilityChanged: (visibility) {
final isVisible = visibility.visibleBounds != Rect.zero; final isVisible = visibility.visibleBounds != Rect.zero;
if (isVisible && if (isVisible && !_bottomWasVisible) {
!_bottomWasVisible && if (widget.parentMessage == null) {
streamChannel.channel.config?.readEvents == true) { streamChannel.queryMessages(direction: QueryDirection.bottom);
if (streamChannel.channel.state.unreadCount > 0) { } else {
streamChannel.getReplies(
widget.parentMessage.id,
direction: QueryDirection.bottom,
);
}
if (streamChannel.channel.config?.readEvents == true &&
streamChannel.channel.state.unreadCount > 0) {
streamChannel.channel.markRead(); streamChannel.channel.markRead();
} }
_bottomWasVisible = !isVisible;
} }
_bottomWasVisible = isVisible;
if (mounted) { if (mounted) {
setState(() { setState(() {
_showScrollToBottom = !isVisible; _showScrollToBottom = !isVisible;
+74 -24
View File
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
enum QueryDirection { top, bottom }
/// Widget used to provide information about the channel to the widget tree /// Widget used to provide information about the channel to the widget tree
/// ///
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance. /// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
@@ -52,34 +54,57 @@ class StreamChannelState extends State<StreamChannel> {
/// The stream notifying the state of queryMessage call /// The stream notifying the state of queryMessage call
Stream<bool> get queryMessage => _queryMessageController.stream; Stream<bool> get queryMessage => _queryMessageController.stream;
bool _paginationEnded = false; bool _topPaginationEnded = false;
bool _bottomPaginationEnded = false;
/// Calls [channel.query] updating [queryMessage] stream /// Calls [channel.query] updating [queryMessage] stream
void queryMessages() { void queryMessages({QueryDirection direction = QueryDirection.top}) {
if (_queryMessageController.value == true || _paginationEnded) { if (_queryMessageController.value == true ||
(_topPaginationEnded && _bottomPaginationEnded)) {
return; return;
} }
_queryMessageController.add(true); _queryMessageController.add(true);
String firstId; String id;
if (channel.state.messages.isNotEmpty) { PaginationParams params;
firstId = channel.state.messages.first.id;
}
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 widget.channel
.query( .query(
messagesPagination: PaginationParams( messagesPagination: params,
lessThan: firstId,
limit: messageLimit,
),
preferOffline: true, preferOffline: true,
) )
.then((res) { .then((res) {
if (res.messages.isEmpty || res.messages.length < messageLimit) { 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); _queryMessageController.add(false);
}).catchError((e, stack) { }).catchError((e, stack) {
@@ -90,35 +115,60 @@ class StreamChannelState extends State<StreamChannel> {
} }
/// Calls [channel.getReplies] updating [queryMessage] stream /// Calls [channel.getReplies] updating [queryMessage] stream
Future<void> getReplies(String parentId) async { Future<void> getReplies(
if (_queryMessageController.value == true || _paginationEnded) { String parentId, {
QueryDirection direction = QueryDirection.top,
}) async {
if (_queryMessageController.value == true ||
(_topPaginationEnded && _bottomPaginationEnded)) {
return; return;
} }
_queryMessageController.add(true); _queryMessageController.add(true);
String firstId; String id;
PaginationParams params;
final messageLimit = 50;
if (widget.channel.state.threads.containsKey(parentId)) { if (widget.channel.state.threads.containsKey(parentId)) {
final thread = widget.channel.state.threads[parentId]; final thread = widget.channel.state.threads[parentId];
if (thread != null && thread.isNotEmpty) { 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 return widget.channel
.getReplies( .getReplies(
parentId, parentId,
PaginationParams( params,
lessThan: firstId,
limit: messageLimit,
),
preferOffline: true, preferOffline: true,
) )
.then((res) { .then((res) {
if (res.messages.isEmpty || res.messages.length < messageLimit) { 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); _queryMessageController.add(false);
}).catchError((e, stack) { }).catchError((e, stack) {
+4 -1
View File
@@ -28,7 +28,10 @@ dependencies:
file_picker: ^2.0.12 file_picker: ^2.0.12
image_picker: ^0.6.7+2 image_picker: ^0.6.7+2
flutter_keyboard_visibility: ^3.3.0 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 mime: ^0.9.6+3
video_compress: ^2.1.1 video_compress: ^2.1.1
visibility_detector: ^0.1.5 visibility_detector: ^0.1.5