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:
|
dependencies:
|
||||||
stream_chat: ^0.2.23+3
|
stream_chat: ^0.2.23+3
|
||||||
|
rxdart: ^0.24.1
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user