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
+75 -61
View File
@@ -294,7 +294,16 @@ 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(
onWillPop: () async {
if (_isSearchActive) {
_controller.clear();
setState(() => _isSearchActive = false);
return false;
}
return true;
},
child: ChannelsBloc(
child: MessageSearchBloc( child: MessageSearchBloc(
child: Column( child: Column(
children: [ children: [
@@ -323,7 +332,48 @@ class _ChannelListPageState extends State<ChannelListPage> {
), ),
], ],
paginationParams: PaginationParams(limit: 20), paginationParams: PaginationParams(limit: 20),
onItemTap: (message) {}, 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( : ChannelListView(
onStartChatPressed: () { onStartChatPressed: () {
@@ -349,75 +399,31 @@ class _ChannelListPageState extends State<ChannelListPage> {
], ],
), ),
), ),
),
); );
} }
} }
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;
+78 -28
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;
if (widget.channel.state.threads.containsKey(parentId)) { PaginationParams params;
final thread = widget.channel.state.threads[parentId];
if (thread != null && thread.isNotEmpty) {
firstId = thread?.first?.id;
}
}
final messageLimit = 50; final messageLimit = 50;
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;
}
}
}
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