+122
-108
@@ -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')
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user