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