diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_view.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_view.dart index 920ee09b..38417190 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_view.dart @@ -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) 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 with WidgetsBindingObserver { - final ScrollController _scrollController = ScrollController(); - @override Widget build(BuildContext context) { final channelsBloc = ChannelsBloc.of(context); @@ -162,10 +147,7 @@ class _ChannelListViewState extends State } } - return AnimatedSwitcher( - child: child, - duration: Duration(milliseconds: 500), - ); + return child; }, ); } @@ -186,19 +168,17 @@ class _ChannelListViewState extends State 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 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 options: widget.options, ); }); + + if (widget.channelListController != null) { + widget.channelListController.paginateData = paginateData; + } } @override @@ -270,3 +246,8 @@ class _ChannelListViewState extends State super.dispose(); } } + +/// Controller used for paginating data in [ChannelListView] +class ChannelListController { + VoidCallback paginateData; +} diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_view.dart b/packages/stream_chat_flutter_core/lib/src/message_list_view.dart new file mode 100644 index 00000000..d0a59266 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_list_view.dart @@ -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: [ +/// 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) 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 { + StreamChannelState streamChannel; + + bool get _upToDate => streamChannel.channel.state.isUpToDate; + + bool get _isThreadConversation => widget.parentMessage != null; + + int initialIndex; + double initialAlignment; + + List messages = []; + + 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>( + 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 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; +} diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_view.dart new file mode 100644 index 00000000..f27f7c8e --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_view.dart @@ -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 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 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 messageFilters; + + final Widget Function(List) 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 { + @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>( + 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; +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat.dart index e6c47c9d..17ec62b6 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat.dart @@ -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 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 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; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_theme.dart deleted file mode 100644 index be584492..00000000 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_theme.dart +++ /dev/null @@ -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(); - - 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 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 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}'; diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_view.dart b/packages/stream_chat_flutter_core/lib/src/user_list_view.dart index 4681d931..1b841333 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_view.dart @@ -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 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 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 with WidgetsBindingObserver { - bool get _isListView => widget.crossAxisCount == 1; - @override void initState() { super.initState(); @@ -150,6 +113,10 @@ class _UserListViewState extends State pagination: widget.pagination, options: widget.options, ); + + if (widget.userListController != null) { + widget.userListController.paginateData = paginateData; + } } @override @@ -214,235 +181,36 @@ class _UserListViewState extends State 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: [ - 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 items) { - final usersProvider = UsersBloc.of(context); - if (i < items.length) { - final item = items[i]; - return item.when( - headerItem: (header) { - return Container( - key: ValueKey('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('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 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('USER-${user.id}'), - child: widget.userItemBuilder(context, user, selected), - ); - }, - ); - } else { - return _buildQueryProgressIndicator(context, usersProvider); - } - } - - Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) { - return StreamBuilder( - 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; +}