feat: Removed theme and added new widgets

This commit is contained in:
Deven Joshi
2021-01-22 17:14:48 +05:30
parent cf3e1b8363
commit c0ebd9efe4
6 changed files with 419 additions and 1238 deletions
@@ -8,14 +8,6 @@ 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);
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png)
///
@@ -57,15 +49,16 @@ class ChannelListView extends StatefulWidget {
this.options,
this.sort,
this.pagination,
this.separatorBuilder,
this.errorBuilder,
this.swipeToAction = false,
this.pullToRefresh = true,
@required this.errorBuilder,
@required this.emptyBuilder,
@required this.loadingBuilder,
@required this.listBuilder,
this.channelListController,
}) : super(key: key);
final ChannelListController channelListController;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
@@ -73,9 +66,6 @@ class ChannelListView extends StatefulWidget {
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;
@@ -102,9 +92,6 @@ class ChannelListView extends StatefulWidget {
/// 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;
@@ -114,8 +101,6 @@ class ChannelListView extends StatefulWidget {
class _ChannelListViewState extends State<ChannelListView>
with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
@@ -162,10 +147,7 @@ class _ChannelListViewState extends State<ChannelListView>
}
}
return AnimatedSwitcher(
child: child,
duration: Duration(milliseconds: 500),
);
return child;
},
);
}
@@ -186,19 +168,17 @@ class _ChannelListViewState extends State<ChannelListView>
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,
);
}
void paginateData() {
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination.copyWith(
offset: channelsBloc.channels?.length ?? 0,
),
options: widget.options,
);
}
StreamSubscription _subscription;
@@ -217,14 +197,6 @@ class _ChannelListViewState extends State<ChannelListView>
options: widget.options,
);
_scrollController.addListener(() {
channelsBloc.queryChannelsLoading.first.then((loading) {
if (!loading) {
_listenChannelPagination(channelsBloc);
}
});
});
final client = StreamChat.of(context).client;
_subscription = client
@@ -242,6 +214,10 @@ class _ChannelListViewState extends State<ChannelListView>
options: widget.options,
);
});
if (widget.channelListController != null) {
widget.channelListController.paginateData = paginateData;
}
}
@override
@@ -270,3 +246,8 @@ class _ChannelListViewState extends State<ChannelListView>
super.dispose();
}
}
/// Controller used for paginating data in [ChannelListView]
class ChannelListController {
VoidCallback paginateData;
}
@@ -0,0 +1,166 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_channel.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png)
///
/// It shows the list of messages of the current channel.
///
/// ```dart
/// class ChannelPage extends StatelessWidget {
/// const ChannelPage({
/// Key key,
/// }) : super(key: key);
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: ChannelHeader(),
/// body: Column(
/// children: <Widget>[
/// Expanded(
/// child: MessageListView(
/// threadBuilder: (_, parentMessage) {
/// return ThreadPage(
/// parent: parentMessage,
/// );
/// },
/// ),
/// ),
/// MessageInput(),
/// ],
/// ),
/// );
/// }
/// }
/// ```
///
///
/// Make sure to have a [StreamChannel] 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 MessageListView extends StatefulWidget {
/// Instantiate a new MessageListView
MessageListView({
Key key,
this.showScrollToBottom = true,
this.parentMessage,
this.dateDividerBuilder,
@required this.loadingBuilder,
@required this.emptyBuilder,
@required this.messageListBuilder,
this.messageListController,
}) : super(key: key);
MessageListController messageListController;
final Widget Function(BuildContext, List<Message>) messageListBuilder;
/// Function used to build a loading widget
final WidgetBuilder loadingBuilder;
/// Function used to build an empty widget
final WidgetBuilder emptyBuilder;
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
final bool showScrollToBottom;
/// Parent message in case of a thread
final Message parentMessage;
/// Builder used to render date dividers
final Widget Function(DateTime) dateDividerBuilder;
@override
_MessageListViewState createState() => _MessageListViewState();
}
class _MessageListViewState extends State<MessageListView> {
StreamChannelState streamChannel;
bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool get _isThreadConversation => widget.parentMessage != null;
int initialIndex;
double initialAlignment;
List<Message> messages = <Message>[];
bool initialMessageHighlightComplete = false;
@override
Widget build(BuildContext context) {
final messagesStream = _isThreadConversation
? streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream;
return StreamBuilder<List<Message>>(
stream: messagesStream?.map((messages) => messages
?.where((e) =>
(!e.isDeleted && e.shadowed != true) ||
(e.isDeleted &&
e.user.id == streamChannel.channel.client.state.user.id))
?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty) {
if (_upToDate) {
return widget.emptyBuilder(context);
}
} else {
messages = messageList;
}
return widget.messageListBuilder(context, messages);
});
}
Future<void> paginateData(QueryDirection direction) {
if (!_isThreadConversation) {
return streamChannel.queryMessages(direction: direction);
} else {
return streamChannel.getReplies(widget.parentMessage.id);
}
}
@override
void initState() {
streamChannel = StreamChannel.of(context);
if (_isThreadConversation) {
streamChannel.getReplies(widget.parentMessage.id);
}
if (widget.messageListController != null) {
widget.messageListController.paginateData = paginateData;
}
super.initState();
}
@override
void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
}
super.dispose();
}
}
/// Controller used for paginating data in [ChannelListView]
class MessageListController {
Function(QueryDirection direction) paginateData;
}
@@ -0,0 +1,194 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'lazy_load_scroll_view.dart';
import 'message_search_bloc.dart';
/// Callback called when tapping on a user
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
/// Builder used to create a custom [ListUserItem] from a [User]
typedef MessageSearchItemBuilder = Widget Function(
BuildContext, GetMessageResponse);
/// Builder used when [MessageSearchListView] is empty
typedef EmptyMessageSearchBuilder = Widget Function(
BuildContext context, String searchQuery);
///
/// It shows the list of searched messages.
///
/// ```dart
/// class MessageSearchPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: MessageSearchListView(
/// messageQuery: _channelQuery,
/// filters: {
/// 'members': {
/// r'$in': [user.id]
/// }
/// },
/// paginationParams: PaginationParams(limit: 20),
/// ),
/// );
/// }
/// }
/// ```
///
///
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
/// The widget uses a [ListView.separated] to render the list of messages.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageSearchListView extends StatefulWidget {
/// Instantiate a new MessageSearchListView
const MessageSearchListView({
Key key,
this.messageQuery,
this.filters,
this.sortOptions,
this.paginationParams,
this.messageFilters,
@required this.emptyBuilder,
@required this.errorBuilder,
@required this.loadingBuilder,
@required this.childBuilder,
this.messageSearchListController,
}) : super(key: key);
final MessageSearchListController messageSearchListController;
/// Message String to search on
final String messageQuery;
/// 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> filters;
/// 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> sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams paginationParams;
/// The message 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> messageFilters;
final Widget Function(List<GetMessageResponse>) childBuilder;
/// The builder used when the channel list is empty.
final EmptyMessageSearchBuilder emptyBuilder;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
final WidgetBuilder loadingBuilder;
@override
_MessageSearchListViewState createState() => _MessageSearchListViewState();
}
class _MessageSearchListViewState extends State<MessageSearchListView> {
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
if (widget.messageSearchListController != null) {
widget.messageSearchListController.paginateData = paginateData;
}
}
@override
Widget build(BuildContext context) {
final messageSearchBloc = MessageSearchBloc.of(context);
return _buildListView(messageSearchBloc);
}
Widget _buildListView(MessageSearchBlocState messageSearchBloc) {
return StreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context, widget.messageQuery);
}
return widget.childBuilder(snapshot.data);
},
);
}
void paginateData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.loadMore(
filter: widget.filters,
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0,
),
query: widget.messageQuery,
messageFilter: widget.messageFilters,
);
}
@override
void didUpdateWidget(MessageSearchListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString() ||
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
widget.messageFilters?.toString() !=
oldWidget.messageFilters?.toString()) {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
}
}
}
/// Controller used for paginating data in [ChannelListView]
class MessageSearchListController {
VoidCallback paginateData;
}
@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_theme.dart';
/// Widget used to provide information about the chat to the widget tree
///
@@ -29,13 +28,11 @@ import 'package:stream_chat_flutter_core/src/stream_chat_theme.dart';
class StreamChat extends StatefulWidget {
final Client client;
final Widget child;
final StreamChatThemeData streamChatThemeData;
StreamChat({
Key key,
@required this.client,
@required this.child,
this.streamChatThemeData,
}) : super(
key: key,
);
@@ -65,32 +62,7 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
final theme = _getTheme(context, widget.streamChatThemeData);
return StreamChatTheme(
data: theme,
child: Builder(
builder: (context) {
final materialTheme = Theme.of(context);
final streamTheme = StreamChatTheme.of(context);
return Theme(
data: materialTheme.copyWith(
primaryIconTheme: streamTheme.primaryIconTheme,
accentColor: streamTheme.colorTheme.accentBlue,
scaffoldBackgroundColor: streamTheme.colorTheme.white,
),
child: widget.child,
);
},
),
);
}
StreamChatThemeData _getTheme(
BuildContext context,
StreamChatThemeData themeData,
) {
final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context));
return defaultTheme.merge(themeData) ?? themeData;
return widget.child;
}
/// The current user
@@ -103,13 +75,6 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// client.state?.totalUnreadCountStream?.listen((count) {
// if (count > 0) {
// FlutterAppBadger.updateBadgeCount(count);
// } else {
// FlutterAppBadger.removeBadge();
// }
// });
}
StreamSubscription _newMessageSubscription;
@@ -1,898 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/reaction_icon.dart';
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
class StreamChatTheme extends InheritedWidget {
final StreamChatThemeData data;
StreamChatTheme({
Key key,
@required this.data,
Widget child,
}) : super(
key: key,
child: child,
);
@override
bool updateShouldNotify(StreamChatTheme old) {
return data != old.data;
}
/// Use this method to get the current [StreamChatThemeData] instance
static StreamChatThemeData of(BuildContext context) {
final streamChatTheme =
context.dependOnInheritedWidgetOfExactType<StreamChatTheme>();
if (streamChatTheme == null) {
throw Exception(
'You must have a StreamChatTheme widget at the top of your widget tree',
);
}
return streamChatTheme.data;
}
}
/// Theme data
class StreamChatThemeData {
/// The text themes used in the widgets
final TextTheme textTheme;
/// The button themes used in the widgets
final ButtonThemeData buttonTheme;
/// The text themes used in the widgets
final ColorTheme colorTheme;
/// Theme of the [ChannelPreview]
final ChannelPreviewTheme channelPreviewTheme;
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
/// Theme of the current user messages
final MessageTheme ownMessageTheme;
/// Theme of other users messages
final MessageTheme otherMessageTheme;
/// The widget that will be built when the channel image is unavailable
final Widget Function(BuildContext, Channel) defaultChannelImage;
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
/// Primary icon theme
final IconThemeData primaryIconTheme;
/// Assets used for rendering reactions
final List<ReactionIcon> reactionIcons;
/// Create a theme from scratch
const StreamChatThemeData({
this.textTheme,
this.buttonTheme,
this.colorTheme,
this.channelPreviewTheme,
this.channelTheme,
this.otherMessageTheme,
this.ownMessageTheme,
this.defaultChannelImage,
this.defaultUserImage,
this.primaryIconTheme,
this.reactionIcons,
});
/// Create a theme from a Material [Theme]
factory StreamChatThemeData.fromTheme(ThemeData theme) {
final defaultTheme = getDefaultTheme(theme);
final customizedTheme = StreamChatThemeData(
primaryIconTheme: theme.primaryIconTheme,
ownMessageTheme: MessageTheme(
replies: TextStyle(color: theme.accentColor),
messageLinks: TextStyle(color: theme.accentColor),
),
otherMessageTheme: MessageTheme(
replies: TextStyle(color: theme.accentColor),
messageLinks: TextStyle(color: theme.accentColor),
),
);
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
}
/// Creates a copy of [StreamChatThemeData] with specified attributes overridden.
StreamChatThemeData copyWith({
TextTheme textTheme,
ButtonThemeData buttonTheme,
ColorTheme colorTheme,
ChannelPreviewTheme channelPreviewTheme,
ChannelTheme channelTheme,
MessageTheme ownMessageTheme,
MessageTheme otherMessageTheme,
Widget Function(BuildContext, Channel) defaultChannelImage,
Widget Function(BuildContext, User) defaultUserImage,
IconThemeData primaryIconTheme,
List<ReactionIcon> reactionIcons,
}) =>
StreamChatThemeData(
textTheme: textTheme ?? this.textTheme,
buttonTheme: buttonTheme ?? this.buttonTheme,
colorTheme: colorTheme ?? this.colorTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
channelPreviewTheme: channelPreviewTheme ?? this.channelPreviewTheme,
channelTheme: channelTheme ?? this.channelTheme,
ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme,
otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme,
reactionIcons: reactionIcons ?? this.reactionIcons,
);
StreamChatThemeData merge(StreamChatThemeData other) {
if (other == null) return this;
return copyWith(
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
buttonTheme: other.buttonTheme,
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
primaryIconTheme: other.primaryIconTheme,
defaultChannelImage: other.defaultChannelImage,
defaultUserImage: other.defaultUserImage,
channelPreviewTheme:
channelPreviewTheme?.merge(other.channelPreviewTheme) ??
other.channelPreviewTheme,
channelTheme:
channelTheme?.merge(other.channelTheme) ?? other.channelTheme,
ownMessageTheme: ownMessageTheme?.merge(other.ownMessageTheme) ??
other.ownMessageTheme,
otherMessageTheme: otherMessageTheme?.merge(other.otherMessageTheme) ??
other.otherMessageTheme,
reactionIcons: other.reactionIcons,
);
}
/// Get the default Stream Chat theme
static StreamChatThemeData getDefaultTheme(ThemeData theme) {
final accentColor = Color(0xff006cff);
final isDark = theme.brightness == Brightness.dark;
final textTheme = isDark ? TextTheme.dark() : TextTheme.light();
final colorTheme = isDark ? ColorTheme.dark() : ColorTheme.light();
return StreamChatThemeData(
textTheme: textTheme,
colorTheme: colorTheme,
buttonTheme: ButtonThemeData(
height: 48.0,
buttonColor: isDark ? Color(0xffffffff) : Color(0xff006aff),
textTheme: ButtonTextTheme.accent,
colorScheme: theme.colorScheme.copyWith(
secondary: isDark ? Color(0xff005eff) : Color(0xffffffff),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
defaultChannelImage: (context, channel) => SizedBox(),
defaultUserImage: (context, user) => Center(
child: Image.network(
getRandomPicUrl(user),
filterQuality: FilterQuality.high,
fit: BoxFit.cover,
),
),
channelPreviewTheme: ChannelPreviewTheme(
unreadCounterColor: colorTheme.accentRed,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
title: textTheme.bodyBold,
subtitle: textTheme.footnote.copyWith(
color: Color(0xff7A7A7A),
),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.black.withOpacity(.5),
),
indicatorIconSize: 16.0),
channelTheme: ChannelTheme(
messageInputButtonIconTheme: theme.iconTheme.copyWith(
color: accentColor,
),
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
color: colorTheme.white,
title: TextStyle(
fontSize: 14,
color: colorTheme.black,
),
lastMessageAt: TextStyle(
fontSize: 11,
color: colorTheme.black.withOpacity(.5),
),
),
inputBackground: colorTheme.white.withAlpha(12),
),
ownMessageTheme: MessageTheme(
messageText: textTheme.body,
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
replies: textTheme.footnoteBold.copyWith(color: accentColor),
messageBackgroundColor: colorTheme.greyGainsboro,
reactionsBackgroundColor: colorTheme.white,
reactionsBorderColor: colorTheme.greyWhisper,
reactionsMaskColor: colorTheme.whiteSnow,
messageBorderColor: colorTheme.greyGainsboro,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 32,
width: 32,
),
),
messageLinks: TextStyle(
color: accentColor,
),
),
otherMessageTheme: MessageTheme(
reactionsBackgroundColor: colorTheme.greyGainsboro,
reactionsBorderColor: colorTheme.white,
reactionsMaskColor: colorTheme.whiteSnow,
messageText: textTheme.body,
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
replies: textTheme.footnoteBold.copyWith(color: accentColor),
messageLinks: TextStyle(
color: accentColor,
),
messageBackgroundColor: colorTheme.white,
messageBorderColor: colorTheme.greyWhisper,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 32,
width: 32,
),
),
),
reactionIcons: [
ReactionIcon(
type: 'love',
assetName: 'Icon_love_reaction.svg',
),
ReactionIcon(
type: 'like',
assetName: 'Icon_thumbs_up_reaction.svg',
),
ReactionIcon(
type: 'sad',
assetName: 'Icon_thumbs_down_reaction.svg',
),
ReactionIcon(
type: 'haha',
assetName: 'Icon_LOL_reaction.svg',
),
ReactionIcon(
type: 'wow',
assetName: 'Icon_wut_reaction.svg',
),
],
);
}
}
enum TextThemeType {
light,
dark,
}
class TextTheme {
final TextStyle title;
final TextStyle headlineBold;
final TextStyle headline;
final TextStyle bodyBold;
final TextStyle body;
final TextStyle footnoteBold;
final TextStyle footnote;
final TextStyle captionBold;
TextTheme.light({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.headlineBold = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.headline = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.bodyBold = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.body = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.footnoteBold = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.footnote = const TextStyle(
fontSize: 12,
color: Colors.black,
),
this.captionBold = const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.black,
),
});
TextTheme.dark({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.headlineBold = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.headline = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.bodyBold = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.body = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.footnoteBold = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.footnote = const TextStyle(
fontSize: 12,
color: Colors.white,
),
this.captionBold = const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
),
});
TextTheme copyWith({
TextThemeType type = TextThemeType.light,
TextStyle body,
TextStyle title,
TextStyle headlineBold,
TextStyle headline,
TextStyle bodyBold,
TextStyle footnoteBold,
TextStyle footnote,
TextStyle captionBold,
}) {
return type == TextThemeType.light
? TextTheme.light(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
headline: headline ?? this.headline,
bodyBold: bodyBold ?? this.bodyBold,
footnoteBold: footnoteBold ?? this.footnoteBold,
footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold,
)
: TextTheme.dark(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
headline: headline ?? this.headline,
bodyBold: bodyBold ?? this.bodyBold,
footnoteBold: footnoteBold ?? this.footnoteBold,
footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold,
);
}
TextTheme merge(TextTheme other) {
if (other == null) return this;
return copyWith(
body: body?.merge(other.body) ?? other.body,
title: title?.merge(other.title) ?? other.title,
headlineBold:
headlineBold?.merge(other.headlineBold) ?? other.headlineBold,
headline: headline?.merge(other.headline) ?? other.headline,
bodyBold: bodyBold?.merge(other.bodyBold) ?? other.bodyBold,
footnoteBold:
footnoteBold?.merge(other.footnoteBold) ?? other.footnoteBold,
footnote: footnote?.merge(other.footnote) ?? other.footnote,
captionBold: captionBold?.merge(other.captionBold) ?? other.captionBold,
);
}
}
enum ColorThemeType {
light,
dark,
}
class ColorTheme {
final Color black;
final Color grey;
final Color greyGainsboro;
final Color greyWhisper;
final Color whiteSmoke;
final Color whiteSnow;
final Color white;
final Color blueAlice;
final Color accentBlue;
final Color accentRed;
final Color accentGreen;
final Effect borderTop;
final Effect borderBottom;
final Effect shadowIconButton;
final Effect modalShadow;
final Color highlight;
final Color overlay;
final Color overlayDark;
final Gradient bgGradient;
ColorTheme.light({
this.black = const Color(0xff000000),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xffdbdbdb),
this.greyWhisper = const Color(0xffecebeb),
this.whiteSmoke = const Color(0xfff2f2f2),
this.whiteSnow = const Color(0xfffcfcfc),
this.white = const Color(0xffffffff),
this.blueAlice = const Color(0xffe9f2ff),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3842),
this.accentGreen = const Color(0xff20E070),
this.highlight = const Color(0xfffbf4dd),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.2),
this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6),
this.bgGradient = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xfff7f7f7), const Color(0xfffcfcfc)],
stops: [0, 1],
),
this.borderTop = const Effect(
sigmaX: 0,
sigmaY: -1,
color: Color(0xff000000),
blur: 0.0,
alpha: 0.08),
this.borderBottom = const Effect(
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0.0, alpha: 0.08),
this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
});
ColorTheme.dark({
this.black = const Color(0xffffffff),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xff2d2f2f),
this.greyWhisper = const Color(0xff1c1e22),
this.whiteSmoke = const Color(0xff13151b),
this.whiteSnow = const Color(0xff070A0D),
this.white = const Color(0xff101418),
this.blueAlice = const Color(0xff00193D),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3742),
this.accentGreen = const Color(0xff20E070),
this.borderTop = const Effect(
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
this.borderBottom = const Effect(
sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0, alpha: 1.0),
this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
this.highlight = const Color(0xff302d22),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6),
this.bgGradient = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xff101214), const Color(0xff070a0d)],
stops: [0, 1],
),
});
ColorTheme copyWith({
ColorThemeType type = ColorThemeType.light,
Color black,
Color grey,
Color greyGainsboro,
Color greyWhisper,
Color whiteSmoke,
Color whiteSnow,
Color white,
Color blueAlice,
Color accentBlue,
Color accentRed,
Color accentGreen,
Effect borderTop,
Effect borderBottom,
Effect shadowIconButton,
Effect modalShadow,
Color highlight,
Color overlay,
Color overlayDark,
Gradient bgGradient,
}) {
return type == ColorThemeType.light
? ColorTheme.light(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
modalShadow: modalShadow ?? this.modalShadow,
highlight: highlight ?? this.highlight,
overlay: overlay ?? this.overlay,
overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient,
)
: ColorTheme.dark(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
modalShadow: modalShadow ?? this.modalShadow,
highlight: highlight ?? this.highlight,
overlay: overlay ?? this.overlay,
overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient,
);
}
ColorTheme merge(ColorTheme other) {
if (other == null) return this;
return copyWith(
black: other.black,
grey: other.grey,
greyGainsboro: other.greyGainsboro,
greyWhisper: other.greyWhisper,
whiteSmoke: other.whiteSmoke,
whiteSnow: other.whiteSnow,
white: other.white,
blueAlice: other.blueAlice,
accentBlue: other.accentBlue,
accentRed: other.accentRed,
accentGreen: other.accentGreen,
highlight: other.highlight,
overlay: other.overlay,
overlayDark: other.overlayDark,
bgGradient: other.bgGradient,
borderTop: other.borderTop,
borderBottom: other.borderBottom,
shadowIconButton: other.shadowIconButton,
modalShadow: other.modalShadow,
);
}
}
/// Channel theme data
class ChannelTheme {
/// Theme of the [ChannelHeader] widget
final ChannelHeaderTheme channelHeaderTheme;
/// IconTheme of the send button in [MessageInput]
final IconThemeData messageInputButtonIconTheme;
/// Theme of the send button in [MessageInput]
final ButtonThemeData messageInputButtonTheme;
/// Background color of [MessageInput]
final Color inputBackground;
ChannelTheme({
this.channelHeaderTheme,
this.messageInputButtonIconTheme,
this.messageInputButtonTheme,
this.inputBackground,
});
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
ChannelTheme copyWith({
ChannelHeaderTheme channelHeaderTheme,
IconThemeData messageInputButtonIconTheme,
ButtonThemeData messageInputButtonTheme,
Color inputBackground,
}) =>
ChannelTheme(
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
messageInputButtonIconTheme:
messageInputButtonIconTheme ?? this.messageInputButtonIconTheme,
messageInputButtonTheme:
messageInputButtonTheme ?? this.messageInputButtonTheme,
inputBackground: inputBackground ?? this.inputBackground,
);
ChannelTheme merge(ChannelTheme other) {
if (other == null) return this;
return copyWith(
channelHeaderTheme: channelHeaderTheme?.merge(other.channelHeaderTheme) ??
other.channelHeaderTheme,
messageInputButtonIconTheme: messageInputButtonIconTheme
?.merge(other.messageInputButtonIconTheme) ??
other.messageInputButtonIconTheme,
messageInputButtonTheme: other.messageInputButtonTheme,
inputBackground: other.inputBackground,
);
}
}
class AvatarTheme {
final BoxConstraints constraints;
final BorderRadius borderRadius;
AvatarTheme({
this.constraints,
this.borderRadius,
});
AvatarTheme copyWith({
BoxConstraints constraints,
BorderRadius borderRadius,
}) =>
AvatarTheme(
constraints: constraints ?? this.constraints,
borderRadius: borderRadius ?? this.borderRadius,
);
AvatarTheme merge(AvatarTheme other) {
if (other == null) return this;
return copyWith(
constraints: other.constraints,
borderRadius: other.borderRadius,
);
}
}
class MessageTheme {
final TextStyle messageText;
final TextStyle messageAuthor;
final TextStyle messageLinks;
final TextStyle createdAt;
final TextStyle replies;
final Color messageBackgroundColor;
final Color messageBorderColor;
final Color reactionsBackgroundColor;
final Color reactionsBorderColor;
final Color reactionsMaskColor;
final AvatarTheme avatarTheme;
const MessageTheme({
this.replies,
this.messageText,
this.messageAuthor,
this.messageLinks,
this.messageBackgroundColor,
this.messageBorderColor,
this.reactionsBackgroundColor,
this.reactionsBorderColor,
this.reactionsMaskColor,
this.avatarTheme,
this.createdAt,
});
MessageTheme copyWith({
TextStyle messageText,
TextStyle messageAuthor,
TextStyle messageLinks,
TextStyle createdAt,
TextStyle replies,
Color messageBackgroundColor,
Color messageBorderColor,
AvatarTheme avatarTheme,
Color reactionsBackgroundColor,
Color reactionsBorderColor,
Color reactionsMaskColor,
}) =>
MessageTheme(
messageText: messageText ?? this.messageText,
messageAuthor: messageAuthor ?? this.messageAuthor,
messageLinks: messageLinks ?? this.messageLinks,
createdAt: createdAt ?? this.createdAt,
messageBackgroundColor:
messageBackgroundColor ?? this.messageBackgroundColor,
messageBorderColor: messageBorderColor ?? this.messageBorderColor,
avatarTheme: avatarTheme ?? this.avatarTheme,
replies: replies ?? this.replies,
reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
);
MessageTheme merge(MessageTheme other) {
if (other == null) return this;
return copyWith(
messageText: messageText?.merge(other.messageText) ?? other.messageText,
messageAuthor:
messageAuthor?.merge(other.messageAuthor) ?? other.messageAuthor,
messageLinks:
messageLinks?.merge(other.messageLinks) ?? other.messageLinks,
createdAt: createdAt?.merge(other.createdAt) ?? other.createdAt,
replies: replies?.merge(other.replies) ?? other.replies,
messageBackgroundColor: other.messageBackgroundColor,
messageBorderColor: other.messageBorderColor,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
reactionsBackgroundColor: other.reactionsBackgroundColor,
reactionsBorderColor: other.reactionsBorderColor,
reactionsMaskColor: other.reactionsMaskColor,
);
}
}
class ChannelPreviewTheme {
final TextStyle title;
final TextStyle subtitle;
final TextStyle lastMessageAt;
final AvatarTheme avatarTheme;
final Color unreadCounterColor;
final double indicatorIconSize;
const ChannelPreviewTheme({
this.title,
this.subtitle,
this.lastMessageAt,
this.avatarTheme,
this.unreadCounterColor,
this.indicatorIconSize,
});
ChannelPreviewTheme copyWith({
TextStyle title,
TextStyle subtitle,
TextStyle lastMessageAt,
AvatarTheme avatarTheme,
Color unreadCounterColor,
double indicatorIconSize,
}) =>
ChannelPreviewTheme(
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme,
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
);
ChannelPreviewTheme merge(ChannelPreviewTheme other) {
if (other == null) return this;
return copyWith(
title: title?.merge(other.title) ?? other.title,
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
lastMessageAt:
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
unreadCounterColor: other.unreadCounterColor,
);
}
}
class ChannelHeaderTheme {
final TextStyle title;
final TextStyle lastMessageAt;
final AvatarTheme avatarTheme;
final Color color;
const ChannelHeaderTheme({
this.title,
this.lastMessageAt,
this.avatarTheme,
this.color,
});
ChannelHeaderTheme copyWith({
TextStyle title,
TextStyle lastMessageAt,
AvatarTheme avatarTheme,
Color color,
}) =>
ChannelHeaderTheme(
title: title ?? this.title,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color,
);
ChannelHeaderTheme merge(ChannelHeaderTheme other) {
if (other == null) return this;
return copyWith(
title: title?.merge(other.title) ?? other.title,
lastMessageAt:
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
color: other.color,
);
}
}
class Effect {
final double sigmaX;
final double sigmaY;
final Color color;
final double alpha;
final double blur;
const Effect({
this.sigmaX,
this.sigmaY,
this.color,
this.alpha,
this.blur,
});
Effect copyWith({
double sigmaX,
double sigmaY,
Color color,
double alpha,
double blur,
}) =>
Effect(
sigmaX: sigmaX ?? this.sigmaX,
sigmaY: sigmaY ?? this.sigmaY,
color: color ?? this.color,
alpha: color ?? this.alpha,
blur: blur ?? this.blur,
);
}
/// Get random png with initials
String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
@@ -2,15 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_theme.dart';
/// Callback called when tapping on a user
typedef UserTapCallback = void Function(User, Widget);
/// Builder used to create a custom [ListUserItem] from a [User]
typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
///
/// It shows the list of current users.
@@ -47,31 +39,30 @@ class UserListView extends StatefulWidget {
/// Instantiate a new UserListView
const UserListView({
Key key,
this.errorBuilder,
this.emptyBuilder,
this.filter,
this.options,
this.sort,
this.pagination,
this.onUserTap,
this.onUserLongPress,
this.userWidget,
this.userItemBuilder,
this.separatorBuilder,
this.onImageTap,
this.selectedUsers,
this.pullToRefresh = true,
this.groupAlphabetically = false,
this.crossAxisCount = 1,
}) : assert(
crossAxisCount == 1 || groupAlphabetically == false,
'Cannot group alphabetically when crossAxisCount > 1',
),
super(key: key);
@required this.errorBuilder,
@required this.emptyBuilder,
@required this.loadingBuilder,
@required this.listBuilder,
this.userListController,
}) : super(key: key);
final UserListController userListController;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
/// The builder that will be used to build the list
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
/// The builder that will be used for loading
final WidgetBuilder loadingBuilder;
/// The builder used when the channel list is empty.
final WidgetBuilder emptyBuilder;
@@ -98,48 +89,20 @@ class UserListView extends StatefulWidget {
/// message_limit: how many messages should be included to each channel
final PaginationParams pagination;
/// Function called when tapping on a channel
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
/// with the widget [userWidget] as child.
final UserTapCallback onUserTap;
/// Function called when long pressing on a channel
final Function(User) onUserLongPress;
/// Widget used when opening a channel
final Widget userWidget;
/// Builder used to create a custom user preview
final UserItemBuilder userItemBuilder;
/// Builder used to create a custom item separator
final Function(BuildContext, int) separatorBuilder;
/// The function called when the image is tapped
final Function(User) onImageTap;
/// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh;
/// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers]
final Set<User> selectedUsers;
/// Set it to true to group users by their first character
///
/// defaults to false
final bool groupAlphabetically;
/// The number of children in the cross axis.
final int crossAxisCount;
@override
_UserListViewState createState() => _UserListViewState();
}
class _UserListViewState extends State<UserListView>
with WidgetsBindingObserver {
bool get _isListView => widget.crossAxisCount == 1;
@override
void initState() {
super.initState();
@@ -150,6 +113,10 @@ class _UserListViewState extends State<UserListView>
pagination: widget.pagination,
options: widget.options,
);
if (widget.userListController != null) {
widget.userListController.paginateData = paginateData;
}
}
@override
@@ -214,235 +181,36 @@ class _UserListViewState extends State<UserListView>
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,
children: <Widget>[
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading channels'),
],
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
usersBlocState.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
},
child: Text('Retry'),
),
],
),
);
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
);
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty && widget.emptyBuilder != null) {
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
if (items.isEmpty && widget.emptyBuilder == null) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Text('There are no users currently'),
),
),
);
},
);
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
final child = _isListView
? ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, items);
},
)
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, items);
},
);
return LazyLoadScrollView(
onEndOfPage: () async {
return _listenUserPagination(usersBlocState);
},
child: child,
);
return widget.listBuilder(context, items);
},
);
}
Widget _listItemBuilder(BuildContext context, int i, List<ListItem> items) {
final usersProvider = UsersBloc.of(context);
if (i < items.length) {
final item = items[i];
return item.when(
headerItem: (header) {
return Container(
key: ValueKey<String>('HEADER-$header'),
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
child: Text(
header,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.5,
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
),
);
},
userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false;
return Container(
key: ValueKey<String>('USER-${user.id}'),
child: widget.userItemBuilder(context, user, selected),
);
},
);
} else {
return _buildQueryProgressIndicator(context, usersProvider);
}
}
void paginateData() {
final usersBloc = UsersBloc.of(context);
Widget _gridItemBuilder(BuildContext context, int i, List<ListItem> items) {
final usersProvider = UsersBloc.of(context);
if (i < items.length) {
final item = items[i];
return item.when(
headerItem: (_) => Offstage(),
userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false;
return Container(
key: ValueKey<String>('USER-${user.id}'),
child: widget.userItemBuilder(context, user, selected),
);
},
);
} else {
return _buildQueryProgressIndicator(context, usersProvider);
}
}
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) {
return StreamBuilder<bool>(
stream: usersProvider.queryUsersLoading,
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text('Error loading users'),
),
),
);
}
return Container(
height: 100,
padding: EdgeInsets.all(32),
child: Center(
child: snapshot.data ? CircularProgressIndicator() : Container(),
),
);
});
}
Widget _separatorBuilder(context, i) {
return Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
);
}
void _listenUserPagination(UsersBlocState usersProvider) {
usersProvider.queryUsers(
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
offset: usersProvider.users?.length ?? 0,
offset: usersBloc.users?.length ?? 0,
),
options: widget.options,
);
@@ -505,3 +273,8 @@ class ListUserItem extends ListItem {
ListUserItem(this.user);
}
/// Controller used for paginating data in [ChannelListView]
class UserListController {
VoidCallback paginateData;
}