feat: Added new widgets
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter_core/src/channels_bloc.dart';
|
||||
|
||||
import 'stream_chat.dart';
|
||||
|
||||
/// Callback called when tapping on a channel
|
||||
typedef ChannelTapCallback = void Function(Channel, Widget);
|
||||
|
||||
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
||||
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
||||
|
||||
typedef ViewInfoCallback = void Function(Channel);
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows the list of current channels.
|
||||
///
|
||||
/// ```dart
|
||||
/// class ChannelListPage extends StatelessWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: ChannelListView(
|
||||
/// filter: {
|
||||
/// 'members': {
|
||||
/// '\$in': [StreamChat.of(context).user.id],
|
||||
/// }
|
||||
/// },
|
||||
/// sort: [SortOption('last_message_at')],
|
||||
/// pagination: PaginationParams(
|
||||
/// limit: 20,
|
||||
/// ),
|
||||
/// channelWidget: ChannelPage(),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels.
|
||||
/// The widget uses a [ListView.custom] to render the list of channels.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelListView extends StatefulWidget {
|
||||
/// Instantiate a new ChannelListView
|
||||
ChannelListView({
|
||||
Key key,
|
||||
this.filter,
|
||||
this.options,
|
||||
this.sort,
|
||||
this.pagination,
|
||||
this.separatorBuilder,
|
||||
this.errorBuilder,
|
||||
this.swipeToAction = false,
|
||||
this.pullToRefresh = true,
|
||||
@required this.emptyBuilder,
|
||||
@required this.loadingBuilder,
|
||||
@required this.listBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The builder that will be used in case of error
|
||||
final Widget Function(Error error) errorBuilder;
|
||||
|
||||
final WidgetBuilder loadingBuilder;
|
||||
|
||||
final Function(BuildContext, List<Channel>) listBuilder;
|
||||
|
||||
/// If true a default swipe to action behaviour will be added to this widget
|
||||
final bool swipeToAction;
|
||||
|
||||
/// The builder used when the channel list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
|
||||
/// The query filters to use.
|
||||
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||
/// You can also filter other built-in channel fields.
|
||||
final Map<String, dynamic> filter;
|
||||
|
||||
/// Query channels options.
|
||||
///
|
||||
/// state: if true returns the Channel state
|
||||
/// watch: if true listen to changes to this Channel in real time.
|
||||
final Map<String, dynamic> options;
|
||||
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sort;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of channels to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams pagination;
|
||||
|
||||
/// Builder used to create a custom item separator
|
||||
final Function(BuildContext, int) separatorBuilder;
|
||||
|
||||
/// Set it to false to disable the pull-to-refresh widget
|
||||
final bool pullToRefresh;
|
||||
|
||||
@override
|
||||
_ChannelListViewState createState() => _ChannelListViewState();
|
||||
}
|
||||
|
||||
class _ChannelListViewState extends State<ChannelListView>
|
||||
with WidgetsBindingObserver {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channelsBloc = ChannelsBloc.of(context);
|
||||
|
||||
if (!widget.pullToRefresh) {
|
||||
return _buildListView(channelsBloc);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
},
|
||||
child: _buildListView(channelsBloc),
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<List<Channel>> _buildListView(
|
||||
ChannelsBlocState channelsBlocState,
|
||||
) {
|
||||
return StreamBuilder<List<Channel>>(
|
||||
stream: channelsBlocState.channelsStream,
|
||||
builder: (context, snapshot) {
|
||||
var child;
|
||||
if (snapshot.hasError) {
|
||||
child = _buildErrorWidget(
|
||||
snapshot,
|
||||
context,
|
||||
channelsBlocState,
|
||||
);
|
||||
} else if (!snapshot.hasData) {
|
||||
child = _buildLoadingWidget();
|
||||
} else {
|
||||
final channels = snapshot.data;
|
||||
|
||||
child = widget.emptyBuilder(context);
|
||||
|
||||
if (channels.isNotEmpty) {
|
||||
return widget.listBuilder(context, channels);
|
||||
}
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
child: child,
|
||||
duration: Duration(milliseconds: 500),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingWidget() {
|
||||
return widget.loadingBuilder(context);
|
||||
}
|
||||
|
||||
Widget _buildErrorWidget(
|
||||
AsyncSnapshot<List<Channel>> snapshot,
|
||||
BuildContext context,
|
||||
ChannelsBlocState channelsBlocState,
|
||||
) {
|
||||
if (snapshot.error is Error) {
|
||||
print((snapshot.error as Error).stackTrace);
|
||||
}
|
||||
|
||||
return widget.errorBuilder(snapshot.error);
|
||||
}
|
||||
|
||||
void _listenChannelPagination(ChannelsBlocState channelsProvider) {
|
||||
if (_scrollController.position.maxScrollExtent ==
|
||||
_scrollController.offset &&
|
||||
_scrollController.offset != 0) {
|
||||
channelsProvider.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination.copyWith(
|
||||
offset: channelsProvider.channels?.length ?? 0,
|
||||
),
|
||||
options: widget.options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
final channelsBloc = ChannelsBloc.of(context);
|
||||
channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
|
||||
_scrollController.addListener(() {
|
||||
channelsBloc.queryChannelsLoading.first.then((loading) {
|
||||
if (!loading) {
|
||||
_listenChannelPagination(channelsBloc);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
_subscription = client
|
||||
.on(
|
||||
EventType.connectionRecovered,
|
||||
EventType.notificationAddedToChannel,
|
||||
EventType.notificationMessageNew,
|
||||
EventType.channelVisible,
|
||||
)
|
||||
.listen((event) {
|
||||
channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ChannelListView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
||||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
||||
widget.pagination?.toJson()?.toString() !=
|
||||
oldWidget.pagination?.toJson()?.toString() ||
|
||||
widget.options?.toString() != oldWidget.options?.toString()) {
|
||||
final channelsBloc = ChannelsBloc.of(context);
|
||||
channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter_core/src/stream_chat.dart';
|
||||
|
||||
/// Widget dedicated to the management of a channel list with pagination
|
||||
class ChannelsBloc extends StatefulWidget {
|
||||
/// The widget child
|
||||
final Widget child;
|
||||
|
||||
/// Set this to true to prevent channels to be brought to the top of the list when a new message arrives
|
||||
final bool lockChannelsOrder;
|
||||
|
||||
/// Comparator used to sort the channels when a message.new event is received
|
||||
final Comparator<Channel> channelsComparator;
|
||||
|
||||
/// Function used to evaluate if a channel should be added to the list when a message.new event is received
|
||||
final bool Function(Event) shouldAddChannel;
|
||||
|
||||
/// Instantiate a new ChannelsBloc
|
||||
const ChannelsBloc({
|
||||
Key key,
|
||||
this.child,
|
||||
this.lockChannelsOrder = false,
|
||||
this.channelsComparator,
|
||||
this.shouldAddChannel,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChannelsBlocState createState() => ChannelsBlocState();
|
||||
|
||||
/// Use this method to get the current [ChannelsBlocState] instance
|
||||
static ChannelsBlocState of(BuildContext context) {
|
||||
ChannelsBlocState streamChatState;
|
||||
|
||||
streamChatState = context.findAncestorStateOfType<ChannelsBlocState>();
|
||||
|
||||
if (streamChatState == null) {
|
||||
throw Exception('You must have a ChannelsBloc widget as ancestor');
|
||||
}
|
||||
|
||||
return streamChatState;
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of the [ChannelsBloc]
|
||||
class ChannelsBlocState extends State<ChannelsBloc>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
/// The current channel list
|
||||
List<Channel> get channels => _channelsController.value;
|
||||
|
||||
/// The current channel list as a stream
|
||||
Stream<List<Channel>> get channelsStream => _channelsController.stream;
|
||||
|
||||
final BehaviorSubject<bool> _queryChannelsLoadingController =
|
||||
BehaviorSubject.seeded(false);
|
||||
|
||||
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
|
||||
|
||||
/// The stream notifying the state of queryChannel call
|
||||
Stream<bool> get queryChannelsLoading =>
|
||||
_queryChannelsLoadingController.stream;
|
||||
|
||||
final List<Channel> _hiddenChannels = [];
|
||||
|
||||
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
|
||||
Future<void> queryChannels({
|
||||
Map<String, dynamic> filter,
|
||||
List<SortOption> sortOptions,
|
||||
PaginationParams paginationParams,
|
||||
Map<String, dynamic> options,
|
||||
bool onlyOffline = false,
|
||||
}) async {
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
if (client.state?.user == null ||
|
||||
_queryChannelsLoadingController.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryChannelsLoadingController.sink.add(true);
|
||||
|
||||
try {
|
||||
final clear = paginationParams == null ||
|
||||
paginationParams.offset == null ||
|
||||
paginationParams.offset == 0;
|
||||
final oldChannels = List<Channel>.from(channels ?? []);
|
||||
final _channels = await client.queryChannels(
|
||||
filter: filter,
|
||||
sort: sortOptions,
|
||||
options: options,
|
||||
paginationParams: paginationParams,
|
||||
onlyOffline: onlyOffline,
|
||||
);
|
||||
|
||||
if (clear) {
|
||||
_channelsController.add(_channels);
|
||||
} else {
|
||||
final l = oldChannels + _channels;
|
||||
_channelsController.add(l);
|
||||
}
|
||||
_queryChannelsLoadingController.sink.add(false);
|
||||
} catch (err, stackTrace) {
|
||||
print(err);
|
||||
print(stackTrace);
|
||||
_queryChannelsLoadingController.addError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
if (!widget.lockChannelsOrder) {
|
||||
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
|
||||
final newChannels = List<Channel>.from(channels ?? []);
|
||||
final index = newChannels.indexWhere((c) => c.cid == e.cid);
|
||||
if (index > -1) {
|
||||
if (index > 0) {
|
||||
final channel = newChannels.removeAt(index);
|
||||
newChannels.insert(0, channel);
|
||||
}
|
||||
} else if (widget.shouldAddChannel != null &&
|
||||
widget.shouldAddChannel(e)) {
|
||||
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
|
||||
if (hiddenIndex > -1) {
|
||||
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
|
||||
_hiddenChannels.removeAt(hiddenIndex);
|
||||
} else {
|
||||
if (client.state?.channels != null &&
|
||||
client.state?.channels[e.cid] != null) {
|
||||
newChannels.insert(0, client.state.channels[e.cid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.channelsComparator != null) {
|
||||
newChannels.sort(widget.channelsComparator);
|
||||
}
|
||||
_channelsController.add(newChannels);
|
||||
}));
|
||||
}
|
||||
|
||||
_subscriptions.add(client.on(EventType.channelHidden).listen((event) async {
|
||||
final newChannels = List<Channel>.from(channels ?? []);
|
||||
final channelIndex = newChannels.indexWhere((c) => c.cid == event.cid);
|
||||
if (channelIndex > -1) {
|
||||
final channel = newChannels.removeAt(channelIndex);
|
||||
_hiddenChannels.add(channel);
|
||||
_channelsController.add(newChannels);
|
||||
}
|
||||
}));
|
||||
|
||||
_subscriptions.add(client
|
||||
.on(EventType.channelDeleted, EventType.notificationRemovedFromChannel)
|
||||
.listen((e) {
|
||||
final channel = e.channel;
|
||||
_channelsController
|
||||
.add(List.from(channels..removeWhere((c) => c.cid == channel.cid)));
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_channelsController.close();
|
||||
_queryChannelsLoadingController.close();
|
||||
_subscriptions.forEach((s) => s.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import 'stream_chat.dart';
|
||||
|
||||
/// Widget dedicated to the management of a message list with pagination
|
||||
class MessageSearchBloc extends StatefulWidget {
|
||||
/// The widget child
|
||||
final Widget child;
|
||||
|
||||
/// Instantiate a new MessageSearchBloc
|
||||
const MessageSearchBloc({
|
||||
Key key,
|
||||
@required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
MessageSearchBlocState createState() => MessageSearchBlocState();
|
||||
|
||||
/// Use this method to get the current [MessageSearchBlocState] instance
|
||||
static MessageSearchBlocState of(BuildContext context) {
|
||||
MessageSearchBlocState state;
|
||||
|
||||
state = context.findAncestorStateOfType<MessageSearchBlocState>();
|
||||
|
||||
if (state == null) {
|
||||
throw Exception('You must have a MessageSearchBloc widget as ancestor');
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of the [MessageSearchBloc]
|
||||
class MessageSearchBlocState extends State<MessageSearchBloc>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
/// The current messages list
|
||||
List<GetMessageResponse> get messageResponses => _messageResponses.value;
|
||||
|
||||
/// The current messages list as a stream
|
||||
Stream<List<GetMessageResponse>> get messagesStream =>
|
||||
_messageResponses.stream;
|
||||
|
||||
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
|
||||
BehaviorSubject();
|
||||
|
||||
final BehaviorSubject<bool> _queryMessagesLoadingController =
|
||||
BehaviorSubject.seeded(false);
|
||||
|
||||
/// The stream notifying the state of queryUsers call
|
||||
Stream<bool> get queryMessagesLoading =>
|
||||
_queryMessagesLoadingController.stream;
|
||||
|
||||
/// Calls [Client.search] updating [messageResponses] stream
|
||||
Future<void> search({
|
||||
Map<String, dynamic> filter,
|
||||
Map<String, dynamic> messageFilter,
|
||||
List<SortOption> sort,
|
||||
String query,
|
||||
PaginationParams pagination,
|
||||
}) async {
|
||||
_messageResponses.add(null);
|
||||
try {
|
||||
final messages = await _search(
|
||||
filter: filter,
|
||||
messageFilter: messageFilter,
|
||||
sort: sort,
|
||||
query: query,
|
||||
pagination: pagination,
|
||||
);
|
||||
_messageResponses.add(messages.results);
|
||||
} catch (err, stk) {
|
||||
_messageResponses.addError(err, stk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls [Client.search] updating [queryMessagesLoading] stream
|
||||
Future<void> loadMore({
|
||||
Map<String, dynamic> filter,
|
||||
Map<String, dynamic> messageFilter,
|
||||
List<SortOption> sort,
|
||||
String query,
|
||||
PaginationParams pagination,
|
||||
}) async {
|
||||
if (_queryMessagesLoadingController.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryMessagesLoadingController.add(true);
|
||||
try {
|
||||
final clear = pagination == null ||
|
||||
pagination.offset == null ||
|
||||
pagination.offset == 0;
|
||||
|
||||
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
|
||||
|
||||
final messages = await _search(
|
||||
filter: filter,
|
||||
messageFilter: messageFilter,
|
||||
sort: sort,
|
||||
query: query,
|
||||
pagination: pagination,
|
||||
);
|
||||
|
||||
if (clear) {
|
||||
_messageResponses.add(messages.results);
|
||||
} else {
|
||||
final temp = oldMessages + messages.results;
|
||||
_messageResponses.add(temp);
|
||||
}
|
||||
|
||||
_queryMessagesLoadingController.add(false);
|
||||
} catch (err, stackTrace) {
|
||||
_queryMessagesLoadingController.addError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<SearchMessagesResponse> _search({
|
||||
Map<String, dynamic> filter,
|
||||
Map<String, dynamic> messageFilter,
|
||||
List<SortOption> sort,
|
||||
String query,
|
||||
PaginationParams pagination,
|
||||
}) {
|
||||
final client = StreamChat.of(context).client;
|
||||
return client.search(
|
||||
filter,
|
||||
sort,
|
||||
query,
|
||||
pagination,
|
||||
messageFilters: messageFilter,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageResponses.close();
|
||||
_queryMessagesLoadingController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
enum QueryDirection { top, bottom }
|
||||
|
||||
/// Widget used to provide information about the channel to the widget tree
|
||||
///
|
||||
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
|
||||
class StreamChannel extends StatefulWidget {
|
||||
const StreamChannel({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.channel,
|
||||
this.showLoading = true,
|
||||
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;
|
||||
|
||||
streamChannelState = context.findAncestorStateOfType<StreamChannelState>();
|
||||
|
||||
if (streamChannelState == null) {
|
||||
throw Exception(
|
||||
'You must have a StreamChannel widget at the top of your widget tree');
|
||||
}
|
||||
|
||||
return streamChannelState;
|
||||
}
|
||||
|
||||
@override
|
||||
StreamChannelState createState() => StreamChannelState();
|
||||
}
|
||||
|
||||
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 _queryTopMessagesController = BehaviorSubject.seeded(false);
|
||||
final _queryBottomMessagesController = BehaviorSubject.seeded(false);
|
||||
|
||||
/// 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;
|
||||
|
||||
Future<void> _queryTopMessages({
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_topPaginationEnded || _queryTopMessagesController?.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryTopMessagesController.add(true);
|
||||
|
||||
if (channel.state.messages.isEmpty) {
|
||||
return _queryTopMessagesController.add(false);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
_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, {
|
||||
int limit = 50,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_topPaginationEnded || _queryTopMessagesController.value) return;
|
||||
_queryTopMessagesController.add(true);
|
||||
|
||||
Message message;
|
||||
if (channel.state.threads.containsKey(parentId)) {
|
||||
final thread = channel.state.threads[parentId];
|
||||
if (thread.isNotEmpty) {
|
||||
message = thread.first;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await channel.getReplies(
|
||||
parentId,
|
||||
PaginationParams(
|
||||
lessThan: message?.id,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (response.messages.isEmpty || response.messages.length < limit) {
|
||||
_topPaginationEnded = true;
|
||||
}
|
||||
_queryTopMessagesController.add(false);
|
||||
} catch (e, stk) {
|
||||
_queryTopMessagesController.addError(e, stk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the channel members and watchers
|
||||
Future<void> queryMembersAndWatchers() async {
|
||||
await widget.channel.query(
|
||||
membersPagination: PaginationParams(
|
||||
offset: channel.state.members?.length,
|
||||
limit: 100,
|
||||
),
|
||||
watchersPagination: PaginationParams(
|
||||
offset: channel.state.watchers?.length,
|
||||
limit: 100,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
///
|
||||
Future<Message> getMessage(String messageId) async {
|
||||
var message = channel.state.messages.firstWhere(
|
||||
(it) => it.id == messageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (message == null) {
|
||||
final response = await channel.getMessagesById([messageId]);
|
||||
message = response.messages.first;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
_queryTopMessagesController.close();
|
||||
_queryBottomMessagesController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = FutureBuilder<List<bool>>(
|
||||
future: Future.wait(_futures),
|
||||
initialData: [
|
||||
channel.state != null,
|
||||
if (initialMessageId != null) false,
|
||||
],
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.error is Error) {
|
||||
print((snapshot.error as Error).stackTrace);
|
||||
}
|
||||
var message = snapshot.error.toString();
|
||||
if (snapshot.error is DioError) {
|
||||
final dioError = snapshot.error as DioError;
|
||||
if (dioError.type == DioErrorType.RESPONSE) {
|
||||
message = dioError.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
}
|
||||
return Center(
|
||||
child: Text(message),
|
||||
);
|
||||
}
|
||||
final initialized = snapshot.data[0];
|
||||
final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
|
||||
if (widget.showLoading && (!initialized || !dataLoaded)) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return widget.child;
|
||||
},
|
||||
);
|
||||
if (initialMessageId != null) {
|
||||
child = Material(child: child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ environment:
|
||||
|
||||
dependencies:
|
||||
stream_chat: ^0.2.23+3
|
||||
rxdart: ^0.24.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
Reference in New Issue
Block a user