diff --git a/packages/flutter_widgets/example/lib/main.dart b/packages/flutter_widgets/example/lib/main.dart index e34252fd..3f1112ba 100644 --- a/packages/flutter_widgets/example/lib/main.dart +++ b/packages/flutter_widgets/example/lib/main.dart @@ -74,7 +74,7 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData.light(), darkTheme: ThemeData.dark(), - themeMode: ThemeMode.dark, + themeMode: ThemeMode.system, onGenerateRoute: AppRoutes.generateRoute, initialRoute: client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, @@ -286,8 +286,86 @@ class _HomePageState extends State { class UserMentionPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: Text('On Pause Right Now!'), + final user = StreamChat.of(context).user; + return MessageSearchBloc( + child: MessageSearchListView( + filters: { + 'members': { + r'$in': [user.id], + }, + }, + messageFilters: { + 'mentioned_users.id': { + r'$contains': user.id, + }, + }, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + showResultCount: false, + emptyBuilder: (_, __) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.mentions( + size: 96, + color: StreamChatTheme.of(context) + .colorTheme + .greyGainsboro, + ), + ), + Text( + 'No mentions exist yet...', + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith( + color: + StreamChatTheme.of(context).colorTheme.grey, + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + onItemTap: (messageResponse) async { + final client = StreamChat.of(context).client; + final message = messageResponse.message; + final channel = client.channel( + messageResponse.channel.type, + id: messageResponse.channel.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), ); } } @@ -371,7 +449,39 @@ class _ChannelListPageState extends State { direction: SortOption.ASC, ), ], + pullToRefresh: false, paginationParams: PaginationParams(limit: 20), + emptyBuilder: (_, query) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.search( + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No results for \"$query\"...', + ), + ], + ), + ), + ), + ); + }, + ); + }, onItemTap: (messageResponse) async { final client = StreamChat.of(context).client; final message = messageResponse.message; @@ -591,6 +701,12 @@ class _ChannelPageState extends State { horizontal: 8, vertical: 4, ), + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: + StreamChatTheme.of(context).colorTheme.grey), ), ), ), diff --git a/packages/flutter_widgets/example/pubspec.yaml b/packages/flutter_widgets/example/pubspec.yaml index 29b38656..512ccadf 100644 --- a/packages/flutter_widgets/example/pubspec.yaml +++ b/packages/flutter_widgets/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.112+115 +version: 1.0.113+116 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/packages/flutter_widgets/lib/src/message_input.dart b/packages/flutter_widgets/lib/src/message_input.dart index e7a5b24d..ee8763ba 100644 --- a/packages/flutter_widgets/lib/src/message_input.dart +++ b/packages/flutter_widgets/lib/src/message_input.dart @@ -22,9 +22,9 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; -import 'extension.dart'; import '../stream_chat_flutter.dart'; +import 'extension.dart'; import 'quoted_message_widget.dart'; import 'stream_channel.dart'; @@ -477,9 +477,7 @@ class MessageInputState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ StreamSvgIcon.lightning( - color: StreamChatTheme.of(context) - .colorTheme - .white, + color: Colors.white, size: 16.0, ), Text( @@ -488,9 +486,8 @@ class MessageInputState extends State { .textTheme .footnote .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .white), + color: Colors.white, + ), ), ], ), @@ -673,7 +670,7 @@ class MessageInputState extends State { if (commands.isNotEmpty) Padding( padding: - const EdgeInsets.only(left: 8.0, top: 8.0), + const EdgeInsets.only(left: 0.0, top: 8.0), child: Row( children: [ Padding( @@ -698,31 +695,51 @@ class MessageInputState extends State { ], ), ), + SizedBox( + height: 10.0, + ), ...commands .map( - (c) => ListTile( - leading: c.name == 'giphy' - ? _buildGiphyIcon() - : null, - title: Text.rich( - TextSpan( - text: '${c.name.capitalize()}', - style: TextStyle( - fontWeight: FontWeight.bold), + (c) => InkWell( + onTap: () { + _setCommand(c); + }, + child: Container( + height: 40.0, + child: Row( children: [ - TextSpan( - text: ' /${c.name} ${c.args}', - style: TextStyle( - fontWeight: FontWeight.w300, + SizedBox( + width: 16.0, + ), + _buildCommandIcon(c.name), + SizedBox( + width: 8.0, + ), + Text.rich( + TextSpan( + text: '${c.name.capitalize()}', + style: TextStyle( + fontWeight: FontWeight.bold), + children: [ + TextSpan( + text: ' /${c.name} ${c.args}', + style: + StreamChatTheme.of(context) + .textTheme + .body + .copyWith( + color: StreamChatTheme + .of(context) + .colorTheme + .grey, + ), + ), + ], ), ), ], ), ), - //subtitle: Text(c.description), - onTap: () { - _setCommand(c); - }, ), ) .toList(), @@ -957,9 +974,22 @@ class MessageInputState extends State { 'svgs/icon_picture_empty_state.svg', package: 'stream_chat_flutter', height: 140, - color: - StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context) + .colorTheme + .greyGainsboro, ), + Text( + 'Please enable access to your photos \nand videos so you can share them with friends.', + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .grey), + textAlign: TextAlign.center, + ), + SizedBox(height: 6.0), Center( child: Text( 'Allow access to your gallery', @@ -1083,28 +1113,87 @@ class MessageInputState extends State { } } - CircleAvatar _buildGiphyIcon() { - if (kIsWeb) { - return CircleAvatar( - backgroundColor: StreamChatTheme.of(context).colorTheme.black, - child: Image.asset( - 'images/giphy_icon.png', - package: 'stream_chat_flutter', - width: 24.0, - height: 24.0, - ), - radius: 12, - ); - } else { - return CircleAvatar( - child: SvgPicture.asset( - 'svgs/giphy_icon.svg', - package: 'stream_chat_flutter', - width: 24.0, - height: 24.0, - ), - radius: 12, - ); + Widget _buildCommandIcon(String iconType) { + switch (iconType) { + case 'giphy': + return CircleAvatar( + child: StreamSvgIcon.giphyIcon( + size: 24.0, + ), + radius: 12, + ); + break; + case 'ban': + return CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue, + child: StreamSvgIcon.Icon_user_delete( + size: 16.0, + color: Colors.white, + ), + radius: 12, + ); + break; + case 'flag': + return CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue, + child: StreamSvgIcon.flag( + size: 14.0, + color: Colors.white, + ), + radius: 12, + ); + break; + case 'imgur': + return CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue, + child: ClipOval( + child: StreamSvgIcon.imgur( + size: 24.0, + ), + ), + radius: 12, + ); + break; + case 'mute': + return CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue, + child: StreamSvgIcon.mute( + size: 16.0, + color: Colors.white, + ), + radius: 12, + ); + break; + case 'unban': + return CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue, + child: StreamSvgIcon.userAdd( + size: 16.0, + color: Colors.white, + ), + radius: 12, + ); + break; + case 'unmute': + return CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue, + child: StreamSvgIcon.volumeUp( + size: 16.0, + color: Colors.white, + ), + radius: 12, + ); + break; + default: + return CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue, + child: StreamSvgIcon.lightning( + size: 16.0, + color: Colors.white, + ), + radius: 12, + ); + break; } } diff --git a/packages/flutter_widgets/lib/src/message_search_bloc.dart b/packages/flutter_widgets/lib/src/message_search_bloc.dart index ab277c4c..67b6455d 100644 --- a/packages/flutter_widgets/lib/src/message_search_bloc.dart +++ b/packages/flutter_widgets/lib/src/message_search_bloc.dart @@ -52,7 +52,7 @@ class MessageSearchBlocState extends State Stream get queryMessagesLoading => _queryMessagesLoadingController.stream; - /// Calls [Client.search] updating [queryMessagesLoading] stream + /// Calls [Client.search] updating [messageResponses] stream Future search({ Map filter, Map messageFilter, @@ -60,10 +60,30 @@ class MessageSearchBlocState extends State String query, PaginationParams pagination, }) async { - final client = StreamChat.of(context).client; + _messageResponses.add(null); + try { + final messages = await _search( + filter: filter, + messageFilter: messageFilter, + sort: sort, + query: query, + pagination: pagination, + ); + _messageResponses.add(messages.results); + } catch (err, stk) { + _messageResponses.addError(err, stk); + } + } - if (client.state?.user == null || - _queryMessagesLoadingController.value == true) { + /// Calls [Client.search] updating [queryMessagesLoading] stream + Future loadMore({ + Map filter, + Map messageFilter, + List sort, + String query, + PaginationParams pagination, + }) async { + if (_queryMessagesLoadingController.value == true) { return; } _queryMessagesLoadingController.add(true); @@ -74,18 +94,18 @@ class MessageSearchBlocState extends State final oldMessages = List.from(messageResponses ?? []); - final messageResponse = await client.search( - filter, - sort, - query, - pagination, - messageFilters: messageFilter, + final messages = await _search( + filter: filter, + messageFilter: messageFilter, + sort: sort, + query: query, + pagination: pagination, ); if (clear) { - _messageResponses.add(messageResponse.results); + _messageResponses.add(messages.results); } else { - final temp = oldMessages + messageResponse.results; + final temp = oldMessages + messages.results; _messageResponses.add(temp); } @@ -95,6 +115,23 @@ class MessageSearchBlocState extends State } } + Future _search({ + Map filter, + Map messageFilter, + List sort, + String query, + PaginationParams pagination, + }) { + final client = StreamChat.of(context).client; + return client.search( + filter, + sort, + query, + pagination, + messageFilters: messageFilter, + ); + } + @override Widget build(BuildContext context) { super.build(context); diff --git a/packages/flutter_widgets/lib/src/message_search_list_view.dart b/packages/flutter_widgets/lib/src/message_search_list_view.dart index 3ff9f151..73a98a2d 100644 --- a/packages/flutter_widgets/lib/src/message_search_list_view.dart +++ b/packages/flutter_widgets/lib/src/message_search_list_view.dart @@ -51,16 +51,18 @@ class MessageSearchListView extends StatefulWidget { /// Instantiate a new MessageSearchListView const MessageSearchListView({ Key key, - @required this.messageQuery, - @required this.filters, + this.messageQuery, + this.filters, this.sortOptions, this.paginationParams, + this.messageFilters, this.emptyBuilder, this.errorBuilder, this.separatorBuilder, this.itemBuilder, this.onItemTap, this.showResultCount = true, + this.pullToRefresh = true, }) : super(key: key); /// Message String to search on @@ -83,6 +85,11 @@ class MessageSearchListView extends StatefulWidget { /// 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; + /// Builder used to create a custom item preview final MessageSearchItemBuilder itemBuilder; @@ -101,6 +108,9 @@ class MessageSearchListView extends StatefulWidget { /// Set it to false to hide total results text final bool showResultCount; + /// Set it to false to disable the pull-to-refresh widget + final bool pullToRefresh; + @override _MessageSearchListViewState createState() => _MessageSearchListViewState(); } @@ -115,6 +125,7 @@ class _MessageSearchListViewState extends State { sort: widget.sortOptions, query: widget.messageQuery, pagination: widget.paginationParams, + messageFilter: widget.messageFilters, ); } @@ -127,9 +138,7 @@ class _MessageSearchListViewState extends State { Widget _separatorBuilder(BuildContext context, int index) { return Container( height: 1, - color: Theme.of(context).brightness == Brightness.dark - ? StreamChatTheme.of(context).colorTheme.white.withOpacity(0.1) - : StreamChatTheme.of(context).colorTheme.black.withOpacity(0.1), + color: StreamChatTheme.of(context).colorTheme.greyWhisper, ); } @@ -205,9 +214,7 @@ class _MessageSearchListViewState extends State { children: [ WidgetSpan( child: Padding( - padding: const EdgeInsets.only( - right: 2.0, - ), + padding: const EdgeInsets.only(right: 2.0), child: Icon(Icons.error_outline), ), ), @@ -217,18 +224,17 @@ class _MessageSearchListViewState extends State { style: Theme.of(context).textTheme.headline6, ), Padding( - padding: const EdgeInsets.only( - top: 16.0, - ), + padding: const EdgeInsets.only(top: 16.0), child: Text(message), ), - FlatButton( + RaisedButton( onPressed: () { messageSearchBloc.search( filter: widget.filters, sort: widget.sortOptions, query: widget.messageQuery, pagination: widget.paginationParams, + messageFilter: widget.messageFilters, ); }, child: Text('Retry'), @@ -279,32 +285,46 @@ class _MessageSearchListViewState extends State { ); } - Widget child; + Widget 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.pullToRefresh) { + child = RefreshIndicator( + onRefresh: () async => messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + messageFilter: widget.messageFilters, + ), + child: child, + ); + } + child = LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( + onEndOfPage: () => messageSearchBloc.loadMore( filter: widget.filters, sort: widget.sortOptions, pagination: widget.paginationParams.copyWith( offset: messageSearchBloc.messageResponses?.length ?? 0, ), query: widget.messageQuery, + messageFilter: widget.messageFilters, ), - 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); - }, - ), + child: child, ); if (widget.showResultCount) { @@ -344,13 +364,16 @@ class _MessageSearchListViewState extends State { jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) || widget.paginationParams?.toJson()?.toString() != oldWidget.paginationParams?.toJson()?.toString() || - widget.messageQuery?.toString() != oldWidget.messageQuery?.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, ); } } diff --git a/packages/flutter_widgets/lib/src/stream_svg_icon.dart b/packages/flutter_widgets/lib/src/stream_svg_icon.dart index ea5a53a5..3e8cfae2 100644 --- a/packages/flutter_widgets/lib/src/stream_svg_icon.dart +++ b/packages/flutter_widgets/lib/src/stream_svg_icon.dart @@ -829,4 +829,52 @@ class StreamSvgIcon extends StatelessWidget { height: size, ); } + + factory StreamSvgIcon.giphyIcon({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'giphy_icon.svg', + color: color, + width: size, + height: size, + ); + } + + factory StreamSvgIcon.imgur({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'imgur.svg', + color: color, + width: size, + height: size, + ); + } + + factory StreamSvgIcon.volumeUp({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'volume-up.svg', + color: color, + width: size, + height: size, + ); + } + + factory StreamSvgIcon.flag({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'flag.svg', + color: color, + width: size, + height: size, + ); + } } diff --git a/packages/flutter_widgets/lib/src/typing_indicator.dart b/packages/flutter_widgets/lib/src/typing_indicator.dart index b13eade6..177e9e21 100644 --- a/packages/flutter_widgets/lib/src/typing_indicator.dart +++ b/packages/flutter_widgets/lib/src/typing_indicator.dart @@ -54,7 +54,7 @@ class TypingIndicator extends StatelessWidget { height: 4, ), Text( - ' ${snapshot.data.map((u) => u.name).join(',')} ${snapshot.data.length == 1 ? 'is' : 'are'} typing', + ' ${snapshot.data[0].name}${snapshot.data.length == 1 ? '' : ' and ${snapshot.data.length - 1} more'} ${snapshot.data.length == 1 ? 'is' : 'are'} typing', maxLines: 1, style: style, ), diff --git a/packages/flutter_widgets/lib/src/url_attachment.dart b/packages/flutter_widgets/lib/src/url_attachment.dart index 1909fc33..130b1109 100644 --- a/packages/flutter_widgets/lib/src/url_attachment.dart +++ b/packages/flutter_widgets/lib/src/url_attachment.dart @@ -34,10 +34,10 @@ class UrlAttachment extends StatelessWidget { margin: EdgeInsets.symmetric(horizontal: 8.0), child: Stack( children: [ - Center( - child: CachedNetworkImage( - imageUrl: urlAttachment.imageUrl, - ), + CachedNetworkImage( + width: double.infinity, + imageUrl: urlAttachment.imageUrl, + fit: BoxFit.cover, ), Positioned( left: 0.0, diff --git a/packages/flutter_widgets/lib/svgs/flag.svg b/packages/flutter_widgets/lib/svgs/flag.svg new file mode 100644 index 00000000..d5903df4 --- /dev/null +++ b/packages/flutter_widgets/lib/svgs/flag.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/flutter_widgets/lib/svgs/giphy_icon.svg b/packages/flutter_widgets/lib/svgs/giphy_icon.svg new file mode 100644 index 00000000..e1a1d4bb --- /dev/null +++ b/packages/flutter_widgets/lib/svgs/giphy_icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/flutter_widgets/lib/svgs/imgur.svg b/packages/flutter_widgets/lib/svgs/imgur.svg new file mode 100644 index 00000000..1df5d71f --- /dev/null +++ b/packages/flutter_widgets/lib/svgs/imgur.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/flutter_widgets/lib/svgs/volume-up.svg b/packages/flutter_widgets/lib/svgs/volume-up.svg new file mode 100644 index 00000000..21f64c35 --- /dev/null +++ b/packages/flutter_widgets/lib/svgs/volume-up.svg @@ -0,0 +1,3 @@ + + +