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
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<List<Message>> 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<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()),
);
},
),
),
],
);
},
);
}
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: <Widget>[
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')
+6 -2
View File
@@ -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:
+13 -6
View File
@@ -480,8 +480,8 @@ class _MessageListViewState extends State<MessageListView> {
} else {
streamChannel.getReplies(widget.parentMessage.id);
}
_topWasVisible = !topIsVisible;
}
_topWasVisible = topIsVisible;
},
);
}
@@ -515,14 +515,21 @@ class _MessageListViewState extends State<MessageListView> {
key: ValueKey<String>('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;
+74 -24
View File
@@ -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<StreamChannel> {
/// The stream notifying the state of queryMessage call
Stream<bool> 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<StreamChannel> {
}
/// Calls [channel.getReplies] updating [queryMessage] stream
Future<void> getReplies(String parentId) async {
if (_queryMessageController.value == true || _paginationEnded) {
Future<void> 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) {
+4 -1
View File
@@ -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