feat: Added channel_list new implementation, fixed upper repo

This commit is contained in:
Deven Joshi
2021-01-25 19:29:32 +05:30
parent 1cbdbf5d6f
commit 0f7a16dc6d
23 changed files with 160 additions and 1297 deletions
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ChannelFileDisplayScreen extends StatefulWidget {
@@ -9,7 +9,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import '../stream_chat_flutter.dart';
import './channel_name.dart';
import 'channel_image.dart';
import 'stream_channel.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png)
@@ -5,16 +5,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/channels_bloc.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_flutter.dart';
import 'channel_bottom_sheet.dart';
import 'channel_preview.dart';
import 'stream_channel.dart';
import 'stream_chat.dart';
/// Callback called when tapping on a channel
typedef ChannelTapCallback = void Function(Channel, Widget);
@@ -158,168 +155,161 @@ class _ChannelListViewState extends State<ChannelListView>
with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController();
final SlidableController _slideController = SlidableController();
final ChannelListController _channelListController = ChannelListController();
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
if (!widget.pullToRefresh) {
return _buildListView(channelsBloc);
_channelListController.loadData();
}
return RefreshIndicator(
onRefresh: () async {
return channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
_channelListController.loadData();
},
child: _buildListView(channelsBloc),
child: ChannelListCore(
channelListController: _channelListController,
listBuilder: (context, list) {
return _buildListView(list);
},
emptyBuilder: (BuildContext context) {
return _buildEmptyWidget();
},
errorBuilder: (Error error) {
return _buildErrorWidget(context);
},
loadingBuilder: (BuildContext context) {
return _buildLoadingWidget();
},
pagination: widget.pagination,
options: widget.options,
sort: widget.sort,
filter: widget.filter,
),
);
}
StreamBuilder<List<Channel>> _buildListView(
ChannelsBlocState channelsBlocState,
Widget _buildListView(
List<Channel> channels,
) {
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;
var child;
if (channels.isEmpty && widget.emptyBuilder != null) {
child = widget.emptyBuilder(context);
}
if (channels.isEmpty && widget.emptyBuilder == null) {
child = LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Stack(
children: [
ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: StreamSvgIcon.message(
size: 136,
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Lets start chatting!',
style: StreamChatTheme.of(context)
.textTheme
.headline,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 52,
),
child: Text(
'How about sending your first message to a friend?',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
),
],
),
),
if (widget.onStartChatPressed != null)
Positioned(
right: 0,
left: 0,
bottom: 32,
child: Center(
child: FlatButton(
onPressed: widget.onStartChatPressed,
child: Text(
'Start a chat',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
),
),
),
),
],
),
);
},
);
}
if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) {
child = GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount),
itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels);
},
);
} else {
child = ListView.separated(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(),
itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, channels);
},
);
if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) {
child = GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount),
itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels);
},
);
} else {
child = ListView.separated(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(),
itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
}
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, channels);
},
);
}
}
return AnimatedSwitcher(
child: child,
duration: Duration(milliseconds: 500),
return AnimatedSwitcher(
child: child,
duration: Duration(milliseconds: 500),
);
}
Widget _buildEmptyWidget() {
if (widget.emptyBuilder != null) {
return widget.emptyBuilder(context);
}
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Stack(
children: [
ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: StreamSvgIcon.message(
size: 136,
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Lets start chatting!',
style: StreamChatTheme.of(context).textTheme.headline,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 52,
),
child: Text(
'How about sending your first message to a friend?',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(
color:
StreamChatTheme.of(context).colorTheme.grey,
),
),
),
],
),
),
if (widget.onStartChatPressed != null)
Positioned(
right: 0,
left: 0,
bottom: 32,
child: Center(
child: FlatButton(
onPressed: widget.onStartChatPressed,
child: Text(
'Start a chat',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
),
),
),
),
],
),
);
},
);
@@ -443,27 +433,8 @@ class _ChannelListViewState extends State<ChannelListView>
}
Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot,
BuildContext context,
ChannelsBlocState channelsBlocState,
) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
if (widget.errorBuilder != null) {
return widget.errorBuilder(snapshot.error);
}
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: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -484,20 +455,9 @@ class _ChannelListViewState extends State<ChannelListView>
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
channelsBlocState.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
_channelListController.loadData();
},
child: Text('Retry'),
),
@@ -714,14 +674,7 @@ class _ChannelListViewState extends State<ChannelListView>
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,
);
_channelListController.paginateData();
}
}
@@ -759,12 +712,7 @@ class _ChannelListViewState extends State<ChannelListView>
EventType.channelVisible,
)
.listen((event) {
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
_channelListController.loadData();
});
}
@@ -777,13 +725,7 @@ class _ChannelListViewState extends State<ChannelListView>
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,
);
_channelListController.loadData();
}
}
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
@@ -3,7 +3,6 @@ import 'package:flutter/rendering.dart';
import 'package:stream_chat/stream_chat.dart';
import '../stream_chat_flutter.dart';
import 'stream_channel.dart';
/// It shows the current [Channel] name using a [Text] widget.
///
@@ -1,185 +0,0 @@
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/src/stream_chat.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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;
}
@@ -6,7 +6,6 @@ import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/image_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:video_player/video_player.dart';
import 'stream_channel.dart';
import '../stream_chat_flutter.dart';
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'image_actions_modal.dart';
import 'stream_channel.dart';
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// True if this header shows the leading back button
@@ -1,148 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
enum _LoadingStatus { LOADING, STABLE }
/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it
/// reaches the bottom/start of the list
class LazyLoadScrollView extends StatefulWidget {
/// The [Widget] that this widget watches for changes on
final Widget child;
/// Called when the [child] reaches the start of the list
final AsyncCallback onStartOfPage;
/// Called when the [child] reaches the end of the list
final AsyncCallback onEndOfPage;
/// Called when the list scrolling starts
final VoidCallback onPageScrollStart;
/// Called when the list scrolling ends
final VoidCallback onPageScrollEnd;
/// Called every time the [child] is in-between the list
final VoidCallback onInBetweenOfPage;
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
final double scrollOffset;
/// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder
final bool isLoading;
/// Initiates a LazyLoadScrollView widget
const LazyLoadScrollView({
Key key,
@required this.child,
this.onStartOfPage,
this.onEndOfPage,
this.onPageScrollStart,
this.onPageScrollEnd,
this.onInBetweenOfPage,
this.isLoading = false,
this.scrollOffset = 100,
}) : assert(child != null),
super(key: key);
@override
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
double _scrollPosition = 0.0;
@override
Widget build(BuildContext context) {
return NotificationListener(
child: widget.child,
onNotification: _onNotification,
);
}
bool _onNotification(Notification notification) {
if (notification is ScrollStartNotification) {
if (widget.onPageScrollStart != null) {
widget.onPageScrollStart();
return true;
}
}
if (notification is ScrollEndNotification) {
if (widget.onPageScrollEnd != null) {
widget.onPageScrollEnd();
return true;
}
}
if (notification is ScrollUpdateNotification) {
final pixels = notification.metrics.pixels;
final maxScrollExtent = notification.metrics.maxScrollExtent;
final minScrollExtent = notification.metrics.minScrollExtent;
final scrollOffset = widget.scrollOffset;
if (pixels > (minScrollExtent + scrollOffset) &&
pixels < (maxScrollExtent - scrollOffset)) {
if (widget.onInBetweenOfPage != null) {
widget.onInBetweenOfPage();
return true;
}
}
final extentBefore = notification.metrics.extentBefore;
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
if (scrollOffset == null || scrollOffset == 0) {
if (extentAfter == 0) {
_onEndOfPage();
}
if (extentBefore == 0) {
_onStartOfPage();
}
} else {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
} else {
if (extentBefore <= scrollOffset) {
_onStartOfPage();
}
}
}
_scrollPosition = pixels;
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
_onEndOfPage();
}
if (notification.overscroll < 0) {
_onStartOfPage();
}
return true;
}
return false;
}
void _onEndOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (widget.onEndOfPage != null) {
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
}
}
void _onStartOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (widget.onStartOfPage != null) {
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
}
}
}
@@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'message_input.dart';
import 'message_widget.dart';
@@ -26,7 +26,6 @@ import 'package:video_compress/video_compress.dart';
import '../stream_chat_flutter.dart';
import 'extension.dart';
import 'quoted_message_widget.dart';
import 'stream_channel.dart';
typedef FileUploader = Future<String> Function(PlatformFile, Channel);
typedef AttachmentThumbnailBuilder = Widget Function(
@@ -8,7 +8,6 @@ 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/info_tile.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/message_widget.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/system_message.dart';
@@ -16,7 +15,6 @@ import 'package:visibility_detector/visibility_detector.dart';
import '../stream_chat_flutter.dart';
import 'date_divider.dart';
import 'stream_channel.dart';
import 'swipeable.dart';
import 'extension.dart';
@@ -1,150 +0,0 @@
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;
}
@@ -4,10 +4,9 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/info_tile.dart';
import 'package:stream_chat_flutter/src/message_search_item.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_flutter.dart';
import 'lazy_load_scroll_view.dart';
import 'message_search_bloc.dart';
/// Callback called when tapping on a user
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
@@ -1,356 +0,0 @@
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;
}
}
@@ -6,6 +6,7 @@ import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget used to provide information about the chat to the widget tree
///
@@ -61,9 +62,8 @@ class StreamChat extends StatefulWidget {
}
/// The current state of the StreamChat widget
class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
class StreamChatState extends State<StreamChat> {
Client get client => widget.client;
Timer _disconnectTimer;
@override
Widget build(BuildContext context) {
@@ -81,7 +81,10 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
accentColor: streamTheme.colorTheme.accentBlue,
scaffoldBackgroundColor: streamTheme.colorTheme.white,
),
child: widget.child,
child: StreamChatCore(
child: widget.child,
client: client,
),
);
},
),
@@ -106,7 +109,6 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
client.state?.totalUnreadCountStream?.listen((count) {
if (count > 0) {
FlutterAppBadger.updateBadgeCount(count);
@@ -115,67 +117,4 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
}
});
}
StreamSubscription _newMessageSubscription;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (state == AppLifecycleState.paused) {
if (client.showLocalNotification != null) {
_newMessageSubscription = client
.on(EventType.messageNew)
.where((e) => e.user?.id != user.id)
.where((e) => e.message.silent != true)
.where((e) => e.message.shadowed != true)
.listen((event) async {
final channel = client.channel(
event.channelType,
id: event.channelId,
);
client.showLocalNotification(
event.message,
ChannelModel(
id: channel.id,
createdAt: channel.createdAt,
extraData: channel.extraData,
type: channel.type,
memberCount: channel.memberCount,
frozen: channel.frozen,
cid: channel.cid,
deletedAt: channel.deletedAt,
config: channel.config,
createdBy: channel.createdBy,
updatedAt: channel.updatedAt,
lastMessageAt: channel.lastMessageAt,
),
);
});
_disconnectTimer = Timer(client.backgroundKeepAlive, () {
client.disconnect();
});
} else {
client.disconnect();
}
} else if (state == AppLifecycleState.resumed) {
_newMessageSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus.value ==
ConnectionStatus.disconnected) {
NotificationService.handleIosMessageQueue(client);
client.connect();
}
}
}
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget to show the current list of typing users
class TypingIndicator extends StatelessWidget {
@@ -2,8 +2,6 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/users_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'user_item.dart';
@@ -513,42 +511,3 @@ class _UserListViewState extends State<UserListView>
}
}
}
abstract class ListItem {
String get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-$header';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
return 'USER-${user.id}';
}
return null;
}
Widget when({
@required Widget Function(String heading) headerItem,
@required Widget Function(User user) userItem,
}) {
if (this is ListHeaderItem) {
return headerItem((this as ListHeaderItem).heading);
}
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
return SizedBox();
}
}
class ListHeaderItem extends ListItem {
final String heading;
ListHeaderItem(this.heading);
}
class ListUserItem extends ListItem {
final User user;
ListUserItem(this.user);
}
@@ -1,108 +0,0 @@
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 users list with pagination
class UsersBloc extends StatefulWidget {
/// The widget child
final Widget child;
/// Instantiate a new UsersBloc
const UsersBloc({
Key key,
@required this.child,
}) : super(key: key);
@override
UsersBlocState createState() => UsersBlocState();
/// Use this method to get the current [UsersBlocState] instance
static UsersBlocState of(BuildContext context) {
UsersBlocState state;
state = context.findAncestorStateOfType<UsersBlocState>();
if (state == null) {
throw Exception('You must have a UsersBloc widget as ancestor');
}
return state;
}
}
/// The current state of the [UsersBloc]
class UsersBlocState extends State<UsersBloc>
with AutomaticKeepAliveClientMixin {
/// The current users list
List<User> get users => _usersController.value;
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
final BehaviorSubject<bool> _queryUsersLoadingController =
BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
/// Calls [Client.queryUsers] updating [queryUsersLoading] stream
Future<void> queryUsers({
Map<String, dynamic> filter,
List<SortOption> sort,
Map<String, dynamic> options,
PaginationParams pagination,
}) async {
final client = StreamChat.of(context).client;
if (client.state?.user == null ||
_queryUsersLoadingController.value == true) {
return;
}
_queryUsersLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
pagination.offset == 0;
final oldUsers = List<User>.from(users ?? []);
final usersResponse = await client.queryUsers(
filter: filter,
sort: sort,
options: options,
pagination: pagination,
);
if (clear) {
_usersController.add(usersResponse.users);
} else {
final temp = oldUsers + usersResponse.users;
_usersController.add(temp);
}
_queryUsersLoadingController.add(false);
} catch (err, stackTrace) {
_queryUsersLoadingController.addError(err, stackTrace);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
void dispose() {
_usersController.close();
_queryUsersLoadingController.close();
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
@@ -7,7 +7,6 @@ export 'src/channel_list_header.dart';
export 'src/channel_list_view.dart';
export 'src/channel_name.dart';
export 'src/channel_preview.dart';
export 'src/channels_bloc.dart';
export 'src/date_divider.dart';
export 'src/deleted_message.dart';
export 'src/file_attachment.dart';
@@ -22,8 +21,6 @@ export 'src/message_text.dart';
export 'src/message_widget.dart';
export 'src/reaction_picker.dart';
export 'src/sending_indicator.dart';
export 'src/stream_channel.dart';
export 'src/stream_chat.dart';
export 'src/stream_chat_theme.dart';
export 'src/stream_neumorphic_button.dart';
export 'src/stream_svg_icon.dart';
@@ -35,11 +32,8 @@ export 'src/user_item.dart';
export 'src/user_item.dart';
export 'src/user_list_view.dart';
export 'src/user_list_view.dart';
export 'src/users_bloc.dart';
export 'src/users_bloc.dart';
export 'src/utils.dart';
export 'src/video_attachment.dart';
export 'src/message_search_bloc.dart';
export 'src/message_search_item.dart';
export 'src/message_search_list_view.dart';
export 'src/unread_indicator.dart';
@@ -47,3 +41,5 @@ export 'src/option_list_tile.dart';
export 'src/channel_file_display_screen.dart';
export 'src/channel_media_display_screen.dart';
export 'src/info_tile.dart';
export 'src/stream_chat.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
+3 -1
View File
@@ -11,6 +11,9 @@ environment:
dependencies:
flutter:
sdk: flutter
# TODO: Change over to main dependency when deployed
stream_chat_flutter_core:
path: ../stream_chat_flutter_core
flutter_app_badger: ^1.1.2
photo_view: ^0.10.3
rxdart: ^0.24.1
@@ -29,7 +32,6 @@ dependencies:
image_picker: ^0.6.7+17
flutter_keyboard_visibility: ^4.0.2
mime: ^0.9.7
stream_chat: ^0.2.24
video_compress: ^2.1.1
visibility_detector: ^0.1.5
http_parser: ^3.1.4
@@ -49,7 +49,6 @@ class ChannelListCore extends StatefulWidget {
this.options,
this.sort,
this.pagination,
this.pullToRefresh = true,
@required this.errorBuilder,
@required this.emptyBuilder,
@required this.loadingBuilder,
@@ -92,9 +91,6 @@ class ChannelListCore extends StatefulWidget {
/// message_limit: how many messages should be included to each channel
final PaginationParams pagination;
/// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh;
@override
_ChannelListCoreState createState() => _ChannelListCoreState();
}
@@ -105,21 +101,7 @@ class _ChannelListCoreState extends State<ChannelListCore>
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),
);
return _buildListView(channelsBloc);
}
StreamBuilder<List<Channel>> _buildListView(