Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2020-12-16 16:56:09 +05:30
parent adefbc1e2d
commit 7495acee3e
11 changed files with 504 additions and 323 deletions
+2 -2
View File
@@ -13,13 +13,13 @@ import 'routes/routes.dart';
const kStreamApiKey = 'STREAM_API_KEY'; const kStreamApiKey = 'STREAM_API_KEY';
const kStreamUserId = 'STREAM_USER_ID'; const kStreamUserId = 'STREAM_USER_ID';
const kStreamToken = 'STREAM_TOKEN'; const kStreamToken = 'STREAM_TOKEN';
const kDefaultStreamApiKey = 'uj7qrdbfrzvg'; const kDefaultStreamApiKey = 's2dxdhpxd94g';
class ChooseUserPage extends StatelessWidget { class ChooseUserPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final users = <String, User>{ final users = <String, User>{
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg':
User( User(
id: 'vishal', id: 'vishal',
extraData: { extraData: {
+1 -1
View File
@@ -133,7 +133,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
context, context,
Routes.CHANNEL_PAGE, Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME), ModalRoute.withName(Routes.HOME),
arguments: channel, arguments: ChannelPageArgs(channel: channel),
); );
}, },
), ),
+11 -36
View File
@@ -337,40 +337,14 @@ class _ChannelListPageState extends State<ChannelListPage> {
final message = messageResponse.message; final message = messageResponse.message;
final channel = Channel.fromState( final channel = Channel.fromState(
client, client,
ChannelState( ChannelState(channel: messageResponse.channel),
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( Navigator.pushNamed(
context, context,
Routes.CHANNEL_PAGE, Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs( arguments: ChannelPageArgs(
channel: channel, channel: channel,
initialScrollIndex: initialIndex, initialMessage: message,
initialAlignment: isFirstMessage ? 0 : 0.5,
), ),
); );
}, },
@@ -406,24 +380,24 @@ class _ChannelListPageState extends State<ChannelListPage> {
class ChannelPageArgs { class ChannelPageArgs {
final Channel channel; final Channel channel;
final int initialScrollIndex; final Message initialMessage;
final double initialAlignment;
const ChannelPageArgs({ const ChannelPageArgs({
this.channel, this.channel,
this.initialScrollIndex = 0, this.initialMessage,
this.initialAlignment = 0,
}); });
} }
class ChannelPage extends StatelessWidget { class ChannelPage extends StatelessWidget {
final int initialScrollIndex; final int initialScrollIndex;
final double initialAlignment; final double initialAlignment;
final bool highlightInitialMessage;
const ChannelPage({ const ChannelPage({
Key key, Key key,
this.initialScrollIndex = 0, this.initialScrollIndex,
this.initialAlignment = 0, this.initialAlignment,
this.highlightInitialMessage = false,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -441,6 +415,7 @@ class ChannelPage extends StatelessWidget {
MessageListView( MessageListView(
initialScrollIndex: initialScrollIndex, initialScrollIndex: initialScrollIndex,
initialAlignment: initialAlignment, initialAlignment: initialAlignment,
highlightInitialMessage: highlightInitialMessage,
threadBuilder: (_, parentMessage) { threadBuilder: (_, parentMessage) {
return ThreadPage( return ThreadPage(
parent: parentMessage, parent: parentMessage,
@@ -481,8 +456,8 @@ class ThreadPage extends StatelessWidget {
ThreadPage({ ThreadPage({
Key key, Key key,
this.parent, this.parent,
this.initialScrollIndex = 0, this.initialScrollIndex,
this.initialAlignment = 0, this.initialAlignment,
}) : super(key: key); }) : super(key: key);
@override @override
+1 -1
View File
@@ -339,7 +339,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
context, context,
Routes.CHANNEL_PAGE, Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME), ModalRoute.withName(Routes.HOME),
arguments: channel, arguments: ChannelPageArgs(channel: channel),
); );
} }
} }
+2 -2
View File
@@ -37,9 +37,9 @@ class AppRoutes {
final arg = args as ChannelPageArgs; final arg = args as ChannelPageArgs;
return StreamChannel( return StreamChannel(
channel: arg.channel, channel: arg.channel,
initialMessageId: arg.initialMessage?.id,
child: ChannelPage( child: ChannelPage(
initialScrollIndex: arg.initialScrollIndex, highlightInitialMessage: arg.initialMessage != null,
initialAlignment: arg.initialAlignment,
), ),
); );
}); });
+23 -14
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum _LoadingStatus { LOADING, STABLE } enum _LoadingStatus { LOADING, STABLE }
@@ -9,10 +10,10 @@ class LazyLoadScrollView extends StatefulWidget {
final Widget child; final Widget child;
/// Called when the [child] reaches the start of the list /// Called when the [child] reaches the start of the list
final VoidCallback onStartOfPage; final AsyncCallback onStartOfPage;
/// Called when the [child] reaches the end of the list /// Called when the [child] reaches the end of the list
final VoidCallback onEndOfPage; final AsyncCallback onEndOfPage;
/// The offset to take into account when triggering [onEndOfPage] in pixels /// The offset to take into account when triggering [onEndOfPage] in pixels
final double scrollOffset; final double scrollOffset;
@@ -38,14 +39,6 @@ class LazyLoadScrollView extends StatefulWidget {
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> { class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE; _LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
@override
void didUpdateWidget(LazyLoadScrollView oldWidget) {
super.didUpdateWidget(oldWidget);
if (!widget.isLoading) {
_loadMoreStatus = _LoadingStatus.STABLE;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return NotificationListener( return NotificationListener(
@@ -62,7 +55,11 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
if (_loadMoreStatus != null && if (_loadMoreStatus != null &&
_loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING; _loadMoreStatus = _LoadingStatus.LOADING;
widget.onEndOfPage(); if (widget.onEndOfPage != null) {
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
} }
} }
if (notification.metrics.minScrollExtent < notification.metrics.pixels && if (notification.metrics.minScrollExtent < notification.metrics.pixels &&
@@ -71,7 +68,11 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
if (_loadMoreStatus != null && if (_loadMoreStatus != null &&
_loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING; _loadMoreStatus = _LoadingStatus.LOADING;
widget.onStartOfPage(); if (widget.onStartOfPage != null) {
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
} }
} }
return true; return true;
@@ -81,14 +82,22 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
if (_loadMoreStatus != null && if (_loadMoreStatus != null &&
_loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING; _loadMoreStatus = _LoadingStatus.LOADING;
widget.onEndOfPage(); if (widget.onEndOfPage != null) {
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
} }
} }
if (notification.overscroll < 0) { if (notification.overscroll < 0) {
if (_loadMoreStatus != null && if (_loadMoreStatus != null &&
_loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING; _loadMoreStatus = _LoadingStatus.LOADING;
widget.onStartOfPage(); if (widget.onStartOfPage != null) {
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
} }
} }
return true; return true;
+8 -2
View File
@@ -21,7 +21,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:substring_highlight/substring_highlight.dart'; import 'package:substring_highlight/substring_highlight.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
import 'package:photo_manager/photo_manager.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
@@ -436,6 +435,7 @@ class MessageInputState extends State<MessageInput> {
} }
Timer _debounce; Timer _debounce;
void _onChanged(BuildContext context, String s) { void _onChanged(BuildContext context, String s) {
if (_debounce?.isActive == true) _debounce.cancel(); if (_debounce?.isActive == true) _debounce.cancel();
_debounce = Timer( _debounce = Timer(
@@ -1859,7 +1859,8 @@ class MessageInputState extends State<MessageInput> {
_mentionsOverlay?.remove(); _mentionsOverlay?.remove();
_mentionsOverlay = null; _mentionsOverlay = null;
final channel = StreamChannel.of(context).channel; final streamChannel = StreamChannel.of(context);
final channel = streamChannel.channel;
Future sendingFuture; Future sendingFuture;
Message message; Message message;
@@ -1885,6 +1886,10 @@ class MessageInputState extends State<MessageInput> {
message = await widget.preMessageSending(message); message = await widget.preMessageSending(message);
} }
if (!channel.state.isUpToDate) {
await streamChannel.reloadChannel();
}
if (widget.editMessage == null || if (widget.editMessage == null ||
widget.editMessage.status == MessageSendingStatus.FAILED) { widget.editMessage.status == MessageSendingStatus.FAILED) {
sendingFuture = channel.sendMessage(message); sendingFuture = channel.sendMessage(message);
@@ -1970,6 +1975,7 @@ class MessageInputState extends State<MessageInput> {
} }
bool _initialized = false; bool _initialized = false;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
if (widget.editMessage != null && !_initialized) { if (widget.editMessage != null && !_initialized) {
+224 -135
View File
@@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:rxdart/rxdart.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
@@ -109,10 +111,11 @@ class MessageListView extends StatefulWidget {
this.onThreadTap, this.onThreadTap,
this.dateDividerBuilder, this.dateDividerBuilder,
this.scrollPhysics = const AlwaysScrollableScrollPhysics(), this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
this.initialScrollIndex = 0, this.initialScrollIndex,
this.initialAlignment = 0, this.initialAlignment,
this.scrollController, this.scrollController,
this.itemPositionListener, this.itemPositionListener,
this.highlightInitialMessage = false,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
@@ -154,6 +157,11 @@ class MessageListView extends StatefulWidget {
/// The ScrollPhysics used by the ListView /// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics; final ScrollPhysics scrollPhysics;
/// If true the list will highlight the initialMessage if there is any.
///
/// Also See [StreamChannel]
final bool highlightInitialMessage;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
@@ -166,6 +174,51 @@ class _MessageListViewState extends State<MessageListView> {
ItemPositionsListener _itemPositionListener; ItemPositionsListener _itemPositionListener;
int _messageListLength; int _messageListLength;
int get _initialIndex {
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
final streamChannel = StreamChannel.of(context);
if (streamChannel.initialMessageId != null) {
final messages = streamChannel.channel.state.messages;
final totalMessages = messages.length;
final messageIndex = messages.indexWhere((e) {
return e.id == streamChannel.initialMessageId;
});
return totalMessages - messageIndex - 1;
}
return 0;
}
double get _initialAlignment {
if (widget.initialAlignment != null) return widget.initialAlignment;
final streamChannel = StreamChannel.of(context);
if (streamChannel.initialMessageId != null) {
final messages = streamChannel.channel.state.messages;
final messageIndex = messages.indexWhere((e) {
return e.id == streamChannel.initialMessageId;
});
final isFirstMessage = messageIndex == 0;
return isFirstMessage ? 0 : 0.5;
}
return 0;
}
bool _isInitialMessage(String id) {
final streamChannel = StreamChannel.of(context);
return streamChannel.initialMessageId == id;
}
bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate;
bool _topPaginationActive = false;
bool _bottomPaginationActive = false;
bool get _paginationActive => _topPaginationActive || _bottomPaginationActive;
int initialIndex;
double initialAlignment;
List<Message> messages = <Message>[];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context); final streamChannel = StreamChannel.of(context);
@@ -176,6 +229,11 @@ class _MessageListViewState extends State<MessageListView> {
.map((threads) => threads[widget.parentMessage.id]) .map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state.messagesStream; : streamChannel.channel.state.messagesStream;
if (!_paginationActive && !_upToDate) {
initialIndex = _initialIndex;
initialAlignment = _initialAlignment;
}
return StreamBuilder<List<Message>>( return StreamBuilder<List<Message>>(
stream: messagesStream.map((messages) => messages stream: messagesStream.map((messages) => messages
.where((e) => .where((e) =>
@@ -190,32 +248,40 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
final messages = snapshot.data?.reversed?.toList() ?? []; final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messages.isEmpty) { if (messageList.isEmpty) {
return Center( if (_upToDate) {
child: Text( return Center(
'No chats here yet...', child: Text(
style: TextStyle( 'No chats here yet...',
fontSize: 12, style: TextStyle(
color: Colors.black.withOpacity(.5), fontSize: 12,
color: Colors.black.withOpacity(.5),
),
), ),
), );
); }
} else {
messages = messageList;
} }
final newMessagesListLength = messages.length; final newMessagesListLength = messages.length;
if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && if (_bottomPaginationActive) {
_messageListLength != null) { if (_itemPositionListener.itemPositions.value?.isNotEmpty == true &&
final first = _itemPositionListener.itemPositions.value.first; _messageListLength != null) {
final diff = newMessagesListLength - _messageListLength; final first = _itemPositionListener.itemPositions.value.first;
if (diff > 0) { final diff = newMessagesListLength - _messageListLength;
_scrollController.jumpTo( if (diff > 0) {
index: first.index + diff, initialIndex = first.index + diff;
alignment: first.itemLeadingEdge, initialAlignment = first.itemLeadingEdge;
); }
} }
} else if (!_topPaginationActive && _upToDate) {
// Reset the index in-case we send any new message
initialIndex = 0;
initialAlignment = 0;
} }
_messageListLength = newMessagesListLength; _messageListLength = newMessagesListLength;
@@ -224,28 +290,32 @@ class _MessageListViewState extends State<MessageListView> {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
LazyLoadScrollView( LazyLoadScrollView(
onStartOfPage: () => _paginateData( onStartOfPage: () async {
streamChannel, if (!_upToDate) {
QueryDirection.bottom, _topPaginationActive = false;
), _bottomPaginationActive = true;
onEndOfPage: () => _paginateData( _paginateData(streamChannel, QueryDirection.bottom);
streamChannel, }
QueryDirection.top, },
), onEndOfPage: () async {
_topPaginationActive = true;
_bottomPaginationActive = false;
_paginateData(streamChannel, QueryDirection.top);
},
child: ScrollablePositionedList.builder( child: ScrollablePositionedList.builder(
key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener, itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true, addAutomaticKeepAlives: true,
key: Key('messageListView'), initialScrollIndex: initialIndex ?? 0,
initialScrollIndex: widget.initialScrollIndex, initialAlignment: initialAlignment ?? 0,
initialAlignment: widget.initialAlignment,
physics: widget.scrollPhysics, physics: widget.scrollPhysics,
itemScrollController: _scrollController, itemScrollController: _scrollController,
reverse: true, reverse: true,
itemCount: messages.length + itemCount: messages.length +
1 + 2 +
(widget.parentMessage != null ? 1 : 0), (widget.parentMessage != null ? 1 : 0),
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == messages.length + 1) { if (i == messages.length + 2) {
if (widget.parentMessageBuilder != null) { if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder( return widget.parentMessageBuilder(
context, context,
@@ -273,16 +343,24 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
} }
if (i == messages.length + 1) {
if (i == messages.length) { return _buildLoadingIndicator(
return _buildLoadingIndicator(streamChannel); streamChannel,
QueryDirection.top,
);
} }
final message = messages[i]; if (i == 0) {
final nextMessage = i > 0 ? messages[i - 1] : null; return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
final message = messages[i - 1];
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
Widget messageWidget; Widget messageWidget;
if (i == 0) { if (i == 1) {
messageWidget = _buildBottomMessage( messageWidget = _buildBottomMessage(
context, context,
message, message,
@@ -339,8 +417,7 @@ class _MessageListViewState extends State<MessageListView> {
}, },
), ),
), ),
if (widget.showScrollToBottom && _showScrollToBottom) if (widget.showScrollToBottom) _buildScrollToBottom(),
_buildScrollToBottom(),
Positioned( Positioned(
top: 20.0, top: 20.0,
child: ValueListenableBuilder<Iterable<ItemPosition>>( child: ValueListenableBuilder<Iterable<ItemPosition>>(
@@ -380,7 +457,7 @@ class _MessageListViewState extends State<MessageListView> {
if (widget.parentMessage == null) { if (widget.parentMessage == null) {
channel.queryMessages(direction: direction); channel.queryMessages(direction: direction);
} else { } else {
channel.getReplies(widget.parentMessage.id, direction: direction); channel.getReplies(widget.parentMessage.id);
} }
} }
@@ -393,91 +470,117 @@ class _MessageListViewState extends State<MessageListView> {
Widget _buildScrollToBottom() { Widget _buildScrollToBottom() {
final streamChannel = StreamChannel.of(context); final streamChannel = StreamChannel.of(context);
return Positioned( return StreamBuilder<Tuple2<bool, int>>(
bottom: 8, stream: Rx.combineLatest2(
right: 8, streamChannel.channel.state.isUpToDateStream,
width: 40, streamChannel.channel.state.unreadCountStream,
height: 40, (bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
child: Stack( ),
clipBehavior: Clip.none, builder: (_, snapshot) {
children: [ if (snapshot.hasError) {
FloatingActionButton( return Offstage();
backgroundColor: Colors.white, } else if (!snapshot.hasData) {
child: StreamSvgIcon.down( return Offstage();
color: Colors.black, }
), final isUpToDate = snapshot.data.item1;
onPressed: () { final showScrollToBottom = !isUpToDate || _showScrollToBottom;
setState(() { if (!showScrollToBottom) {
_showScrollToBottom = false; return Offstage();
}); }
_scrollController.scrollTo( final unreadCount = snapshot.data.item2;
index: 0, final showUnreadCount = unreadCount > 0 &&
duration: Duration(seconds: 1), streamChannel.channel.state.members.any(
curve: Curves.easeInOut, (e) => e.userId == streamChannel.channel.client.state.user.id);
); return Positioned(
}, bottom: 8,
), right: 8,
if (streamChannel.channel.state.members.any((Member e) => width: 40,
e.userId == streamChannel.channel.client.state.user.id)) height: 40,
StreamBuilder<int>( child: Stack(
stream: streamChannel.channel.state.unreadCountStream, clipBehavior: Clip.none,
initialData: streamChannel.channel.state.unreadCount, children: [
builder: (context, snapshot) { FloatingActionButton(
if (!snapshot.hasData || snapshot.data <= 0) { backgroundColor: Colors.white,
return Offstage(); child: StreamSvgIcon.down(
color: Colors.black,
),
onPressed: () {
if (!_upToDate) {
_bottomPaginationActive = false;
_topPaginationActive = false;
streamChannel.reloadChannel();
} else {
setState(() => _showScrollToBottom = false);
_scrollController.scrollTo(
index: 0,
duration: Duration(seconds: 1),
curve: Curves.easeInOut,
);
} }
return Positioned( },
width: 20, ),
height: 20, if (showUnreadCount)
left: 10, Positioned(
top: -10, width: 20,
child: CircleAvatar( height: 20,
child: Padding( left: 10,
padding: const EdgeInsets.all(3.0), top: -10,
child: Text( child: CircleAvatar(
snapshot.data.toString(), child: Padding(
style: TextStyle( padding: const EdgeInsets.all(3.0),
fontSize: 11, child: Text(
fontWeight: FontWeight.bold, snapshot.data.toString(),
), style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
), ),
), ),
), ),
); ),
}), ),
], ],
), ),
);
},
); );
} }
Container _buildLoadingIndicator(StreamChannelState streamChannel) { Widget _buildLoadingIndicator(
return Container( StreamChannelState streamChannel,
key: Key('LOADING-INDICATOR'), QueryDirection direction,
height: 50, ) {
width: double.infinity, final stream = direction == QueryDirection.top
child: StreamBuilder<bool>( ? streamChannel.queryTopMessages
stream: streamChannel.queryMessage, : streamChannel.queryBottomMessages;
initialData: false, return StreamBuilder<bool>(
builder: (context, snapshot) { key: Key('LOADING-INDICATOR'),
if (snapshot.hasError) { stream: stream,
return Container( initialData: false,
color: Color(0xffd0021B).withAlpha(26), builder: (context, snapshot) {
child: Center( if (snapshot.hasError) {
child: Text('Error loading messages'), return Container(
), color: Color(0xffd0021B).withAlpha(26),
); child: Center(
} child: Text('Error loading messages'),
if (!snapshot.data) {
return SizedBox();
}
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
), ),
); );
}), }
); if (!snapshot.data) {
if (direction == QueryDirection.top) {
return Container(
height: 50,
width: double.infinity,
);
}
return Offstage();
}
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: const CupertinoActivityIndicator(),
),
);
});
} }
Widget _buildTopMessage( Widget _buildTopMessage(
@@ -544,9 +647,7 @@ class _MessageListViewState extends State<MessageListView> {
_bottomWasVisible = !isVisible; _bottomWasVisible = !isVisible;
} }
if (mounted) { if (mounted) {
setState(() { setState(() => _showScrollToBottom = !isVisible);
_showScrollToBottom = !isVisible;
});
} }
}, },
child: messageWidget, child: messageWidget,
@@ -602,7 +703,7 @@ class _MessageListViewState extends State<MessageListView> {
final userId = StreamChat.of(context).user.id; final userId = StreamChat.of(context).user.id;
final isMyMessage = message.user.id == userId; final isMyMessage = message.user.id == userId;
final isNextUser = final isNextUser =
index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id; index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id;
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
final readList = channel.state?.read final readList = channel.state?.read
@@ -673,24 +774,12 @@ class _MessageListViewState extends State<MessageListView> {
_messageNewListener = _messageNewListener =
streamChannel.channel.on(EventType.messageNew).listen((event) { streamChannel.channel.on(EventType.messageNew).listen((event) {
final firstElementInViewport =
_itemPositionListener.itemPositions.value.first;
if (event.message.user.id == streamChannel.channel.client.state.user.id) { if (event.message.user.id == streamChannel.channel.client.state.user.id) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.jumpTo( _scrollController.jumpTo(
index: 0, index: 0,
); );
}); });
} else {
if (firstElementInViewport.index != 0) {
_scrollController.jumpTo(
index: firstElementInViewport.index + 1,
alignment: firstElementInViewport.itemLeadingEdge,
);
}
}
if (firstElementInViewport.index == 0) {
streamChannel.channel.markRead();
} }
}); });
+225 -126
View File
@@ -10,19 +10,23 @@ enum QueryDirection { top, bottom }
/// ///
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance. /// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
class StreamChannel extends StatefulWidget { class StreamChannel extends StatefulWidget {
StreamChannel({ const StreamChannel({
Key key, Key key,
@required this.child, @required this.child,
@required this.channel, @required this.channel,
this.showLoading = true, this.showLoading = true,
}) : super( this.initialMessageId,
key: key, }) : assert(child != null),
); assert(channel != null),
super(key: key);
final Widget child; final Widget child;
final Channel channel; final Channel channel;
final bool showLoading; final bool showLoading;
/// If passed the channel will load from this particular message.
final String initialMessageId;
/// Use this method to get the current [StreamChannelState] instance /// Use this method to get the current [StreamChannelState] instance
static StreamChannelState of(BuildContext context) { static StreamChannelState of(BuildContext context) {
StreamChannelState streamChannelState; StreamChannelState streamChannelState;
@@ -45,135 +49,123 @@ class StreamChannelState extends State<StreamChannel> {
/// Current channel /// Current channel
Channel get channel => widget.channel; Channel get channel => widget.channel;
/// InitialMessageId
String get initialMessageId => widget.initialMessageId;
/// Current channel state stream /// Current channel state stream
Stream<ChannelState> get channelStateStream => Stream<ChannelState> get channelStateStream =>
widget.channel.state.channelStateStream; widget.channel.state.channelStateStream;
final BehaviorSubject<bool> _queryMessageController = BehaviorSubject(); final _queryTopMessagesController = BehaviorSubject.seeded(false);
final _queryBottomMessagesController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryMessage call /// The stream notifying the state of [_queryTopMessages] call
Stream<bool> get queryMessage => _queryMessageController.stream; Stream<bool> get queryTopMessages => _queryTopMessagesController.stream;
/// The stream notifying the state of [_queryBottomMessages] call
Stream<bool> get queryBottomMessages => _queryBottomMessagesController.stream;
bool _topPaginationEnded = false; bool _topPaginationEnded = false;
bool _bottomPaginationEnded = false; bool _bottomPaginationEnded = false;
/// Calls [channel.query] updating [queryMessage] stream Future<void> _queryTopMessages({
void queryMessages({QueryDirection direction = QueryDirection.top}) { int limit = 20,
if (_queryMessageController.value == true || bool preferOffline = false,
(_topPaginationEnded && _bottomPaginationEnded)) { }) async {
if (_topPaginationEnded || _queryTopMessagesController?.value == true) {
return; return;
} }
_queryTopMessagesController.add(true);
_queryMessageController.add(true); if (channel.state.messages.isEmpty) {
return _queryTopMessagesController.add(false);
String id;
PaginationParams params;
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 final oldestMessage = channel.state.messages.first;
.query(
messagesPagination: params, try {
preferOffline: true, final state = await queryBeforeMessage(
) oldestMessage.id,
.then((res) { limit: limit,
if (res.messages.isEmpty || res.messages.length < messageLimit) { preferOffline: preferOffline,
switch (direction) { );
case QueryDirection.top: if (state.messages.isEmpty || state.messages.length < limit) {
_topPaginationEnded = true; _topPaginationEnded = true;
break;
case QueryDirection.bottom:
_bottomPaginationEnded = true;
break;
}
} }
_queryMessageController.add(false); _queryTopMessagesController.add(false);
}).catchError((e, stack) { } catch (e, stk) {
if (!_queryMessageController.isClosed) { _queryTopMessagesController.addError(e, stk);
_queryMessageController.addError(e, stack); }
}
Future<void> _queryBottomMessages({
int limit = 20,
bool preferOffline = false,
}) async {
if (_bottomPaginationEnded ||
_queryBottomMessagesController?.value == true ||
channel?.state?.isUpToDate == true) return;
_queryBottomMessagesController.add(true);
if (channel.state.messages.isEmpty) {
return _queryBottomMessagesController.add(false);
}
final recentMessage = channel.state.messages.last;
try {
final state = await queryAfterMessage(
recentMessage.id,
limit: limit,
preferOffline: preferOffline,
);
if (state.messages.isEmpty || state.messages.length < limit) {
_bottomPaginationEnded = true;
} }
}); _queryBottomMessagesController.add(false);
} catch (e, stk) {
_queryBottomMessagesController.addError(e, stk);
}
}
/// Calls [channel.query] updating [queryMessage] stream
Future<void> queryMessages({QueryDirection direction = QueryDirection.top}) {
if (direction == QueryDirection.top) return _queryTopMessages();
return _queryBottomMessages();
} }
/// Calls [channel.getReplies] updating [queryMessage] stream /// Calls [channel.getReplies] updating [queryMessage] stream
Future<void> getReplies( Future<void> getReplies(
String parentId, { String parentId, {
QueryDirection direction = QueryDirection.top, int limit = 50,
bool preferOffline = false,
}) async { }) async {
if (_queryMessageController.value == true || if (_topPaginationEnded || _queryTopMessagesController.value) return;
(_topPaginationEnded && _bottomPaginationEnded)) { _queryTopMessagesController.add(true);
return;
if (!channel.state.threads.containsKey(parentId)) {
return _queryTopMessagesController.add(false);
} }
_queryMessageController.add(true); final thread = channel.state.threads[parentId];
String id; if (thread.isEmpty) return _queryTopMessagesController.add(false);
PaginationParams params;
final messageLimit = 50; final message = thread.first;
if (widget.channel.state.threads.containsKey(parentId)) { try {
final thread = widget.channel.state.threads[parentId]; final state = await queryBeforeMessage(
if (thread != null && thread.isNotEmpty) { message.id,
switch (direction) { limit: limit,
case QueryDirection.top: preferOffline: preferOffline,
id = thread?.first?.id; );
params = PaginationParams( if (state.messages.isEmpty || state.messages.length < limit) {
lessThan: id, _topPaginationEnded = true;
limit: messageLimit,
);
break;
case QueryDirection.bottom:
id = thread?.last?.id;
params = PaginationParams(
greaterThan: id,
limit: messageLimit,
);
break;
}
} }
_queryTopMessagesController.add(false);
} catch (e, stk) {
_queryTopMessagesController.addError(e, stk);
} }
return widget.channel
.getReplies(
parentId,
params,
preferOffline: true,
)
.then((res) {
if (res.messages.isEmpty || res.messages.length < messageLimit) {
switch (direction) {
case QueryDirection.top:
_topPaginationEnded = true;
break;
case QueryDirection.bottom:
_bottomPaginationEnded = true;
break;
}
}
_queryMessageController.add(false);
}).catchError((e, stack) {
_queryMessageController.addError(e, stack);
});
} }
/// Query the channel members and watchers /// Query the channel members and watchers
@@ -190,41 +182,148 @@ class StreamChannelState extends State<StreamChannel> {
); );
} }
/// Loads channel at specific message
Future<void> loadChannelAtMessage(
String messageId, {
int before = 20,
int after = 20,
bool preferOffline = false,
}) {
return queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
}
///
Future<void> queryAtMessage({
String messageId,
int before = 20,
int after = 20,
bool preferOffline = false,
}) async {
if (channel.state == null) return;
channel.state.isUpToDate = false;
channel.state.truncate();
if (messageId == null) {
await channel.query(
messagesPagination: PaginationParams(
limit: before,
),
preferOffline: preferOffline,
);
channel.state.isUpToDate = true;
return;
}
return Future.wait([
queryBeforeMessage(
messageId,
limit: before,
preferOffline: preferOffline,
),
queryAfterMessage(
messageId,
limit: after,
preferOffline: preferOffline,
),
]);
}
///
Future<ChannelState> queryBeforeMessage(
String messageId, {
int limit = 20,
bool preferOffline = false,
}) {
return channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
}
///
Future<ChannelState> queryAfterMessage(
String messageId, {
int limit = 20,
bool preferOffline = false,
}) async {
final state = await channel.query(
messagesPagination: PaginationParams(
greaterThanOrEqual: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
if (state.messages.isEmpty || state.messages.length < limit) {
channel.state.isUpToDate = true;
}
return state;
}
/// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30);
List<Future<bool>> _futures;
Future<bool> get _loadChannelAtMessage async {
try {
await loadChannelAtMessage(initialMessageId);
return true;
} catch (e, stk) {
print('Error: $e\nStack: $stk');
rethrow;
}
}
@override
void initState() {
super.initState();
_futures = [widget.channel.initialized];
if (initialMessageId != null) {
_futures.add(_loadChannelAtMessage);
}
}
@override @override
void dispose() { void dispose() {
_queryMessageController.close(); _queryTopMessagesController.close();
_queryBottomMessagesController.close();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.channel == null) { Widget child = FutureBuilder<List<bool>>(
return Center( future: Future.wait(_futures),
child: CircularProgressIndicator(), initialData: [
); channel.state != null,
} if (initialMessageId != null) false,
return FutureBuilder<bool>( ],
future: widget.channel.initialized,
initialData: widget.channel.state != null,
builder: (context, snapshot) { builder: (context, snapshot) {
if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) { final initialized = snapshot.data[0];
return Container( final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
height: 30, if (widget.showLoading && (!initialized || !dataLoaded)) {
child: Center( return Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
),
); );
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
return Container( return Center(
height: 30, child: Text(snapshot.error),
child: Center(
child: Text(snapshot.error),
),
); );
} else {
return widget.child;
} }
return widget.child;
}, },
); );
if (initialMessageId != null) {
child = Material(child: child);
}
return child;
} }
} }
+3 -1
View File
@@ -339,7 +339,9 @@ class _UserListViewState extends State<UserListView>
); );
return LazyLoadScrollView( return LazyLoadScrollView(
onEndOfPage: () => _listenUserPagination(usersBlocState), onEndOfPage: () async {
return _listenUserPagination(usersBlocState);
},
child: child, child: child,
); );
}, },
+4 -3
View File
@@ -29,9 +29,10 @@ dependencies:
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: stream_chat:
git: path: ../stream-chat-dart
url: https://github.com/GetStream/stream-chat-dart # git:
ref: two-way-pagination # 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