diff --git a/.gitignore b/.gitignore index 3a158cd4..1d3bae26 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,5 @@ doc/api/ fvm google-services.json -example/ios/dist \ No newline at end of file +example/ios/dist +.vscode/ \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 50ff18b0..26590237 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -12,6 +12,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'notifications_service.dart'; import 'routes/app_routes.dart'; import 'routes/routes.dart'; +import 'search_text_field.dart'; +import 'dart:async'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -66,7 +68,45 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { + @override + _ChannelListPageState createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + TextEditingController _controller; + + String _channelQuery = ''; + + bool _isSearchActive = false; + + Timer _debounce; + + void _channelQueryListener() { + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted) { + setState(() { + _channelQuery = _controller.text; + _isSearchActive = _channelQuery.isNotEmpty; + }); + } + }); + } + + @override + void initState() { + super.initState(); + _controller = TextEditingController()..addListener(_channelQueryListener); + } + + @override + void dispose() { + _controller?.removeListener(_channelQueryListener); + _controller?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final user = StreamChat.of(context).user; @@ -79,26 +119,61 @@ class ChannelListPage extends StatelessWidget { drawer: _buildDrawer(context, user), drawerEdgeDragWidth: 50, body: ChannelsBloc( - child: ChannelListView( - onStartChatPressed: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - swipeToAction: true, - filter: { - 'members': { - '\$in': [user.id], - }, - 'draft': { - r'$ne': true, - }, - }, - options: { - 'presence': true, - }, - pagination: PaginationParams( - limit: 20, + child: MessageSearchBloc( + child: Column( + children: [ + SearchTextField( + controller: _controller, + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: _isSearchActive + ? MessageSearchListView( + messageQuery: _channelQuery, + filters: { + 'members': { + r'$in': [user.id] + } + }, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onItemTap: (message) {}, + ) + : ChannelListView( + onStartChatPressed: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); + }, + swipeToAction: true, + filter: { + 'members': { + r'$in': [user.id], + }, + 'draft': { + r'$ne': true, + }, + }, + options: { + 'presence': true, + }, + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + ), + ), + ), + ), + ], ), - channelWidget: ChannelPage(), ), ), ); @@ -207,6 +282,68 @@ class ChannelListPage extends StatelessWidget { } } +class ChannelQuerySearchResultPage extends StatelessWidget { + final Stream> searchResultStream; + + const ChannelQuerySearchResultPage({ + Key key, + @required this.searchResultStream, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + initialData: const [], + stream: searchResultStream, + builder: (context, snapshot) { + final result = snapshot.data; + return Column( + children: [ + if (result.isNotEmpty) + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '${result.length} results', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + Expanded( + child: ListView.builder( + itemCount: result.length, + itemBuilder: (context, index) { + return ListTile( + leading: UserAvatar(), + title: Text(result[index].toJson().toString()), + ); + }, + ), + ), + ], + ); + }, + ); + } +} + class ChannelPage extends StatelessWidget { const ChannelPage({ Key key, @@ -215,6 +352,7 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: ChannelHeader( showTypingIndicator: false, ), diff --git a/example/lib/search_text_field.dart b/example/lib/search_text_field.dart index f46a48b0..e32bb88b 100644 --- a/example/lib/search_text_field.dart +++ b/example/lib/search_text_field.dart @@ -78,7 +78,7 @@ class SearchTextField extends StatelessWidget { Future.microtask( () => [ controller.clear(), - onChanged(''), + if (onChanged != null) onChanged(''), ], ); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e0be8fc9..b66e085b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.78+80 +version: 1.0.80+82 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart index 4272b0f3..48ce8887 100644 --- a/lib/src/channel_bottom_sheet.dart +++ b/lib/src/channel_bottom_sheet.dart @@ -30,10 +30,13 @@ class ChannelBottomSheet extends StatelessWidget { vertical: 2.0, ), child: Center( - child: ChannelName( + child: StreamChannel( + showLoading: false, channel: channel, - textStyle: - StreamChatTheme.of(context).channelPreviewTheme.title, + child: ChannelName( + textStyle: + StreamChatTheme.of(context).channelPreviewTheme.title, + ), ), ), ), diff --git a/lib/src/channel_name.dart b/lib/src/channel_name.dart index 337321ed..bf3eff22 100644 --- a/lib/src/channel_name.dart +++ b/lib/src/channel_name.dart @@ -12,60 +12,65 @@ class ChannelName extends StatelessWidget { /// Instantiate a new ChannelName const ChannelName({ Key key, - this.channel, this.textStyle, }) : super(key: key); - /// The channel to show the name of - final Channel channel; - /// The style of the text displayed final TextStyle textStyle; @override Widget build(BuildContext context) { final client = StreamChat.of(context); - final channel = this.channel ?? StreamChannel.of(context).channel; + final channel = StreamChannel.of(context).channel; + + return StreamBuilder>( + stream: channel.extraDataStream, + initialData: channel.extraData, + builder: (context, snapshot) { + return _buildName(snapshot.data, channel.state.members, client); + }, + ); + } + + Widget _buildName( + Map extraData, + List members, + StreamChatState client, + ) { return LayoutBuilder( builder: (context, constraints) { - return StreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, snapshot) { - String title; - if (snapshot.data['name'] == null) { - final otherMembers = channel.state.members - .where((member) => member.userId != client.user.id); - if (otherMembers.isNotEmpty) { - final maxWidth = constraints.maxWidth; - final maxChars = maxWidth / textStyle.fontSize; - int currentChars = 0; - final currentMembers = []; - otherMembers.forEach((element) { - final newLength = currentChars + element.user.name.length; - if (newLength < maxChars) { - currentChars = newLength; - currentMembers.add(element); - } - }); - - final exceedingMembers = - otherMembers.length - currentMembers.length; - title = - '${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; - } else { - title = channel.id; + String title; + if (extraData['name'] == null) { + final otherMembers = + members.where((member) => member.userId != client.user.id); + if (otherMembers.isNotEmpty) { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / textStyle.fontSize; + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = currentChars + element.user.name.length; + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); } - } else { - title = snapshot.data['name']; - } + }); - return Text( - title, - style: textStyle, - overflow: TextOverflow.ellipsis, - ); - }, + final exceedingMembers = + otherMembers.length - currentMembers.length; + title = + '${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } else { + title = 'No title'; + } + } else { + title = extraData['name']; + } + + return Text( + title, + style: textStyle, + overflow: TextOverflow.ellipsis, ); }, ); diff --git a/lib/src/message_search_bloc.dart b/lib/src/message_search_bloc.dart new file mode 100644 index 00000000..0128bb22 --- /dev/null +++ b/lib/src/message_search_bloc.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'stream_chat.dart'; + +/// Widget dedicated to the management of a message list with pagination +class MessageSearchBloc extends StatefulWidget { + /// The widget child + final Widget child; + + /// Instantiate a new MessageSearchBloc + const MessageSearchBloc({ + Key key, + @required this.child, + }) : super(key: key); + + @override + MessageSearchBlocState createState() => MessageSearchBlocState(); + + /// Use this method to get the current [MessageSearchBlocState] instance + static MessageSearchBlocState of(BuildContext context) { + MessageSearchBlocState state; + + state = context.findAncestorStateOfType(); + + if (state == null) { + throw Exception('You must have a MessageSearchBloc widget as ancestor'); + } + + return state; + } +} + +/// The current state of the [MessageSearchBloc] +class MessageSearchBlocState extends State + with AutomaticKeepAliveClientMixin { + /// The current messages list + List get messageResponses => _messageResponses.value; + + /// The current messages list as a stream + Stream> get messagesStream => + _messageResponses.stream; + + final BehaviorSubject> _messageResponses = + BehaviorSubject(); + + final BehaviorSubject _queryMessagesLoadingController = + BehaviorSubject.seeded(false); + + /// The stream notifying the state of queryUsers call + Stream get queryMessagesLoading => + _queryMessagesLoadingController.stream; + + /// Calls [Client.search] updating [queryMessagesLoading] stream + Future search({ + Map filter, + List sort, + String query, + PaginationParams pagination, + }) async { + final client = StreamChat.of(context).client; + + if (client.state?.user == null || + _queryMessagesLoadingController.value == true) { + return; + } + _queryMessagesLoadingController.add(true); + try { + final clear = pagination == null || + pagination.offset == null || + pagination.offset == 0; + + final oldMessages = List.from(messageResponses ?? []); + + final messageResponse = await client.search( + filter, + sort, + query, + pagination, + ); + + if (clear) { + _messageResponses.add(messageResponse.results); + } else { + final temp = oldMessages + messageResponse.results; + _messageResponses.add(temp); + } + + _queryMessagesLoadingController.add(false); + } catch (err, stackTrace) { + _queryMessagesLoadingController.addError(err, stackTrace); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + void dispose() { + _messageResponses.close(); + _queryMessagesLoadingController.close(); + super.dispose(); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/src/message_search_item.dart b/lib/src/message_search_item.dart new file mode 100644 index 00000000..3e786b9a --- /dev/null +++ b/lib/src/message_search_item.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// It shows the current [Message] preview. +/// +/// Usually you don't use this widget as it's the default item used by [MessageSearchListView]. +/// +/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class MessageSearchItem extends StatelessWidget { + /// Instantiate a new MessageSearchItem + const MessageSearchItem({ + Key key, + @required this.getMessageResponse, + this.onTap, + this.showOnlineStatus = true, + }) : super(key: key); + + /// [Message] displayed + final GetMessageResponse getMessageResponse; + + /// Function called when tapping this widget + final VoidCallback onTap; + + /// If true the [MessageSearchItem] will show the current online Status + final bool showOnlineStatus; + + @override + Widget build(BuildContext context) { + final message = getMessageResponse.message; + final channel = getMessageResponse.channel; + final channelName = channel.extraData['name']; + final user = message.user; + return ListTile( + onTap: onTap, + leading: UserAvatar( + user: user, + showOnlineStatus: showOnlineStatus, + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + title: Row( + children: [ + Text( + user.id == StreamChat.of(context).user.id ? 'You' : user.name, + style: StreamChatTheme.of(context).channelPreviewTheme.title, + ), + if (channelName != null) ...[ + Text( + ' in ', + style: StreamChatTheme.of(context) + .channelPreviewTheme + .title + .copyWith( + fontWeight: FontWeight.normal, + ), + ), + Text( + channelName, + style: StreamChatTheme.of(context).channelPreviewTheme.title, + ), + ], + ], + ), + subtitle: Row( + children: [ + Expanded(child: _buildSubtitle(context, message)), + SizedBox(width: 16), + _buildDate(context, message), + ], + ), + ); + } + + Widget _buildDate(BuildContext context, Message message) { + final lastUpdatedAt = message.updatedAt; + String stringDate; + final now = DateTime.now(); + + if (now.year != lastUpdatedAt.year || + now.month != lastUpdatedAt.month || + now.day != lastUpdatedAt.day) { + stringDate = Jiffy(lastUpdatedAt.toLocal()).format('dd/MM/yyyy'); + } else { + stringDate = Jiffy(lastUpdatedAt.toLocal()).format('HH:mm'); + } + + return Text( + stringDate, + style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, + ); + } + + Widget _buildSubtitle(BuildContext context, Message message) { + if (message == null) { + return SizedBox(); + } + + var text = message.text; + if (message.isDeleted) { + text = 'This message was deleted.'; + } else if (message.attachments != null) { + final parts = [ + ...message.attachments.map((e) { + if (e.type == 'image') { + return '📷'; + } else if (e.type == 'video') { + return '🎬'; + } else if (e.type == 'giphy') { + return '[GIF]'; + } + return null; + }).where((e) => e != null), + message.text ?? '', + ]; + + text = parts.join(' '); + } + + return Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( + color: + StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, + fontStyle: (message.isSystem || message.isDeleted) + ? FontStyle.italic + : FontStyle.normal, + ), + ); + } +} diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart new file mode 100644 index 00000000..8b346570 --- /dev/null +++ b/lib/src/message_search_list_view.dart @@ -0,0 +1,358 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/message_search_item.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); + +/// +/// 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, + @required this.messageQuery, + @required this.filters, + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + this.errorBuilder, + this.separatorBuilder, + this.itemBuilder, + this.onItemTap, + this.showResultCount = true, + }) : super(key: key); + + /// 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; + + /// Builder used to create a custom item preview + final MessageSearchItemBuilder itemBuilder; + + /// Function called when tapping on a [MessageSearchItem] + final MessageSearchItemTapCallback onItemTap; + + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + + /// The builder that will be used in case of error + final Widget Function(Error error) errorBuilder; + + /// Builder used to create a custom item separator + final IndexedWidgetBuilder separatorBuilder; + + /// Set it to false to hide total results text + final bool showResultCount; + + @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, + ); + } + + @override + Widget build(BuildContext context) { + final messageSearchBloc = MessageSearchBloc.of(context); + return _buildListView(messageSearchBloc); + } + + Widget _separatorBuilder(BuildContext context, int index) { + return Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ); + } + + Widget _listItemBuilder( + BuildContext context, GetMessageResponse getMessageResponse) { + if (widget.itemBuilder != null) { + return widget.itemBuilder(context, getMessageResponse); + } + return MessageSearchItem( + getMessageResponse: getMessageResponse, + onTap: () => widget.onItemTap(getMessageResponse), + ); + } + + Widget _buildQueryProgressIndicator( + context, MessageSearchBlocState messageSearchBloc) { + return StreamBuilder( + stream: messageSearchBloc.queryMessagesLoading, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Container( + color: Color(0xffd0021B).withAlpha(26), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center( + child: Text('Error loading messages'), + ), + ), + ); + } + return Container( + height: 100, + padding: EdgeInsets.all(32), + child: Center( + child: snapshot.data ? CircularProgressIndicator() : Container(), + ), + ); + }); + } + + 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); + } + + 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 messages'), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + Padding( + padding: const EdgeInsets.only( + top: 16.0, + ), + child: Text(message), + ), + FlatButton( + onPressed: () { + messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + ); + }, + child: Text('Retry'), + ), + ], + ), + ); + } + + if (!snapshot.hasData) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, + ); + } + + final items = snapshot.data; + + if (items.isEmpty && widget.emptyBuilder != null) { + 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 messages currently'), + ), + ), + ); + }, + ); + } + + Widget child; + child = LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + pagination: widget.paginationParams.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + query: widget.messageQuery, + ), + child: 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) { + if (index < items.length) { + return _listItemBuilder(context, items[index]); + } + return _buildQueryProgressIndicator(context, messageSearchBloc); + }, + ), + ); + + if (widget.showResultCount) { + child = Column( + children: [ + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '${items.length} results', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + Expanded(child: child), + ], + ); + } + return child; + }, + ); + } + + @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()) { + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + ); + } + } +} diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index d5b2c1c2..54b096d1 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -243,9 +243,9 @@ class StreamChatThemeData { ), ), title: TextStyle( - fontSize: 14, - color: isDark ? Colors.white : Colors.black, - ), + fontSize: 14, + color: isDark ? Colors.white : Colors.black, + fontWeight: FontWeight.bold), subtitle: TextStyle( fontSize: 12.5, color: (isDark ? Colors.white : Colors.black).withOpacity(0.5), diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 569eb38a..b57e2d55 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -6,7 +6,6 @@ import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'stream_chat.dart'; import 'user_item.dart'; /// Callback called when tapping on a user @@ -41,8 +40,8 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool); /// ``` /// /// -/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels. -/// The widget uses a [ListView.custom] to render the list of channels. +/// Make sure to have a [UsersBloc] ancestor in order to provide the information about the users. +/// The widget uses a [ListView.separated], [GridView.builder] to render the list, grid of channels. /// /// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// Modify it to change the widget appearance. @@ -63,7 +62,6 @@ class UserListView extends StatefulWidget { this.separatorBuilder, this.onImageTap, this.selectedUsers, - this.swipeToAction = false, this.pullToRefresh = true, this.groupAlphabetically = false, this.crossAxisCount = 1, @@ -76,9 +74,6 @@ class UserListView extends StatefulWidget { /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; - /// 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; @@ -321,7 +316,6 @@ class _UserListViewState extends State final child = _isListView ? ListView.separated( physics: AlwaysScrollableScrollPhysics(), - // controller: _scrollController, itemCount: items.isNotEmpty ? items.length + 1 : items.length, separatorBuilder: (_, index) { if (widget.separatorBuilder != null) { diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index f8ad25ea..35239cbd 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -38,3 +38,6 @@ export 'src/users_bloc.dart'; export 'src/users_bloc.dart'; export 'src/utils.dart'; export 'src/video_attachment.dart'; +export 'src/message_search_bloc.dart'; +export 'src/message_search_item.dart'; +export 'src/message_search_list_view.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 5ecb60de..eee90a9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.13+1 + stream_chat: ^0.2.14 mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5