From 29868aa7a99a5eb732bc8402f58e583623a9c3ed Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 26 Nov 2020 17:46:41 +0100 Subject: [PATCH 01/28] fix messageinput ui --- lib/src/message_input.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 10fe6595..8def16da 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -380,6 +380,7 @@ class MessageInputState extends State { autofocus: false, textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( + isDense: true, hintText: _getHint(), prefixText: _commandEnabled ? null : ' ', border: OutlineInputBorder( @@ -392,7 +393,10 @@ class MessageInputState extends State { borderSide: BorderSide(color: Colors.transparent)), disabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent)), - contentPadding: EdgeInsets.all(8), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 13, + ), prefixIcon: _commandEnabled ? Padding( padding: @@ -665,8 +669,8 @@ class MessageInputState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ IconButton( + iconSize: 24, icon: StreamSvgIcon.pictures( - size: 24, color: _filePickerIndex == 0 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -678,8 +682,8 @@ class MessageInputState extends State { }, ), IconButton( + iconSize: 32, icon: StreamSvgIcon.files( - size: 24, color: _filePickerIndex == 1 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -689,8 +693,8 @@ class MessageInputState extends State { }, ), IconButton( + iconSize: 24, icon: StreamSvgIcon.camera( - size: 24, color: _filePickerIndex == 2 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -700,8 +704,9 @@ class MessageInputState extends State { }, ), IconButton( + padding: const EdgeInsets.all(0), + iconSize: 24, icon: StreamSvgIcon.record( - size: 24, color: _filePickerIndex == 3 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), From ded26db306532289498d9048473e0e6e74385721 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 12:54:57 +0530 Subject: [PATCH 02/28] feat : MessageSearchListView Signed-off-by: xsahil03x --- example/lib/main.dart | 167 ++++++++++++-- lib/src/message_search_bloc.dart | 109 +++++++++ lib/src/message_search_item.dart | 98 +++++++++ lib/src/message_search_list_view.dart | 305 ++++++++++++++++++++++++++ lib/src/stream_chat_theme.dart | 1 + lib/stream_chat_flutter.dart | 3 + 6 files changed, 663 insertions(+), 20 deletions(-) create mode 100644 lib/src/message_search_bloc.dart create mode 100644 lib/src/message_search_item.dart create mode 100644 lib/src/message_search_list_view.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 50ff18b0..bab4b1e0 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,50 @@ 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: _isSearchActive + ? MessageSearchListView( + messageQuery: _channelQuery, + filters: { + 'members': { + r'$in': [user.id] + } + }, + paginationParams: PaginationParams(limit: 20), + ) + : 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 +271,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 +341,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/lib/src/message_search_bloc.dart b/lib/src/message_search_bloc.dart new file mode 100644 index 00000000..bff2d68e --- /dev/null +++ b/lib/src/message_search_bloc.dart @@ -0,0 +1,109 @@ +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 messages => _messagesController.value; + + /// The current messages list as a stream + Stream> get messagesStream => _messagesController.stream; + + final BehaviorSubject> _messagesController = 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(messages ?? []); + + final messageResponse = await client.search( + filter, + sort, + query, + pagination, + ); + + if (clear) { + _messagesController.add(messageResponse.results); + } else { + final temp = oldMessages + messageResponse.results; + _messagesController.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() { + _messagesController.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..d97db708 --- /dev/null +++ b/lib/src/message_search_item.dart @@ -0,0 +1,98 @@ +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'; + +class MessageSearchItem extends StatelessWidget { + final Message message; + + const MessageSearchItem({ + Key key, + @required this.message, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final data = Message.fromJson(message.extraData['message']); + final user = data.user; + debugPrint(message.toJson().toString()); + return ListTile( + leading: UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + title: Text( + user.name, + style: StreamChatTheme.of(context).channelPreviewTheme.title, + ), + subtitle: Row( + children: [ + Expanded(child: _buildSubtitle(context, data)), + _buildDate(context, data), + ], + ), + ); + } + + 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..f317a677 --- /dev/null +++ b/lib/src/message_search_list_view.dart @@ -0,0 +1,305 @@ +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'; + +class MessageSearchListView extends StatefulWidget { + final String messageQuery; + final Map filters; + final List sortOptions; + final PaginationParams paginationParams; + final bool pullToRefresh; + + /// 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; + + const MessageSearchListView({ + Key key, + @required this.messageQuery, + @required this.filters, + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + this.errorBuilder, + this.separatorBuilder, + this.pullToRefresh = true, + }) : super(key: key); + + @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); + + if (!widget.pullToRefresh) { + return _buildListView(messageSearchBloc); + } + + return RefreshIndicator( + onRefresh: () async { + return messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + ); + }, + child: _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, Message message) { + return MessageSearchItem(message: message); + } + + 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.messages?.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 (true) { + 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..9c24525c 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -245,6 +245,7 @@ class StreamChatThemeData { title: TextStyle( fontSize: 14, color: isDark ? Colors.white : Colors.black, + fontWeight: FontWeight.bold ), subtitle: TextStyle( fontSize: 12.5, 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'; From c6552be54062b0fa0aa724b2056bd926404a754b Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 13:42:10 +0530 Subject: [PATCH 03/28] add docComments Signed-off-by: xsahil03x --- lib/src/message_search_item.dart | 23 +++++++- lib/src/message_search_list_view.dart | 76 +++++++++++++++++++++------ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/lib/src/message_search_item.dart b/lib/src/message_search_item.dart index d97db708..a89f2ee0 100644 --- a/lib/src/message_search_item.dart +++ b/lib/src/message_search_item.dart @@ -3,22 +3,40 @@ 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 { - final Message message; - + /// Instantiate a new MessageSearchItem const MessageSearchItem({ Key key, @required this.message, + this.onTap, + this.showOnlineStatus = true, }) : super(key: key); + /// [Message] displayed + final Message message; + + /// 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 data = Message.fromJson(message.extraData['message']); final user = data.user; debugPrint(message.toJson().toString()); return ListTile( + onTap: onTap, leading: UserAvatar( user: user, + showOnlineStatus: showOnlineStatus, constraints: BoxConstraints.tightFor( height: 40, width: 40, @@ -31,6 +49,7 @@ class MessageSearchItem extends StatelessWidget { subtitle: Row( children: [ Expanded(child: _buildSubtitle(context, data)), + SizedBox(width: 16), _buildDate(context, data), ], ), diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart index f317a677..9ad013db 100644 --- a/lib/src/message_search_list_view.dart +++ b/lib/src/message_search_list_view.dart @@ -7,22 +7,36 @@ import 'package:stream_chat_flutter/src/message_search_item.dart'; import 'lazy_load_scroll_view.dart'; import 'message_search_bloc.dart'; +/// +/// 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 { - final String messageQuery; - final Map filters; - final List sortOptions; - final PaginationParams paginationParams; - final bool pullToRefresh; - - /// 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; - + /// Instantiate a new MessageSearchListView const MessageSearchListView({ Key key, @required this.messageQuery, @@ -35,6 +49,38 @@ class MessageSearchListView extends StatefulWidget { this.pullToRefresh = 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; + + /// Set it to false to disable the pull-to-refresh widget + final bool pullToRefresh; + + /// 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; + @override _MessageSearchListViewState createState() => _MessageSearchListViewState(); } From ad37e1b46542b3485ad578bb7dde60996137f925 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 13:44:04 +0530 Subject: [PATCH 04/28] [UserListView] fix docComments Signed-off-by: xsahil03x --- lib/src/user_list_view.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 569eb38a..cc763971 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. @@ -321,7 +320,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) { From 5867dc3317a99c9d30a674ad3411df1be02cec26 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 13:45:28 +0530 Subject: [PATCH 05/28] [UserListView] remove redundant property Signed-off-by: xsahil03x --- lib/src/user_list_view.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index cc763971..b57e2d55 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -62,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, @@ -75,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; From 9ca86067903318140990625b0a4cd297cb586fa3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 27 Nov 2020 09:23:12 +0100 Subject: [PATCH 06/28] fix bottom sheet and channel name --- lib/src/channel_bottom_sheet.dart | 5 +-- lib/src/channel_list_view.dart | 5 ++- lib/src/channel_name.dart | 62 ++++++++++++++++++++----------- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart index c0e32509..4272b0f3 100644 --- a/lib/src/channel_bottom_sheet.dart +++ b/lib/src/channel_bottom_sheet.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_info.dart'; import 'channel_name.dart'; @@ -12,13 +13,11 @@ import 'user_avatar.dart'; class ChannelBottomSheet extends StatelessWidget { const ChannelBottomSheet({ Key key, - @required this.channel, }) : super(key: key); - final Channel channel; - @override Widget build(BuildContext context) { + final channel = StreamChannel.of(context).channel; return SafeArea( child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 45c53eb6..1a431672 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -511,7 +511,10 @@ class _ChannelListViewState extends State ), context: context, builder: (context) { - return ChannelBottomSheet(channel: channel); + return StreamChannel( + child: ChannelBottomSheet(), + channel: channel, + ); }, ); }, diff --git a/lib/src/channel_name.dart b/lib/src/channel_name.dart index 01abcb81..337321ed 100644 --- a/lib/src/channel_name.dart +++ b/lib/src/channel_name.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:stream_chat/stream_chat.dart'; import '../stream_chat_flutter.dart'; @@ -25,29 +26,46 @@ class ChannelName extends StatelessWidget { Widget build(BuildContext context) { final client = StreamChat.of(context); final channel = this.channel ?? StreamChannel.of(context).channel; - 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 exceedingMembers = otherMembers.length - 5; - title = - '${otherMembers.take(5).map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; - } else { - title = channel.id; - } - } else { - title = snapshot.data['name']; - } + 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); + } + }); - 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 = channel.id; + } + } else { + title = snapshot.data['name']; + } + + return Text( + title, + style: textStyle, + overflow: TextOverflow.ellipsis, + ); + }, ); }, ); From 4df3bf161034e92a0108585513d7b696fef8b272 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 13:58:33 +0530 Subject: [PATCH 07/28] minor changes Signed-off-by: xsahil03x --- lib/src/message_search_list_view.dart | 35 ++++++++++++--------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart index 9ad013db..285e452b 100644 --- a/lib/src/message_search_list_view.dart +++ b/lib/src/message_search_list_view.dart @@ -7,6 +7,9 @@ import 'package:stream_chat_flutter/src/message_search_item.dart'; import 'lazy_load_scroll_view.dart'; import 'message_search_bloc.dart'; +/// Builder used to create a custom [ListUserItem] from a [User] +typedef MessageSearchItemBuilder = Widget Function(BuildContext, Message); + /// /// It shows the list of searched messages. /// @@ -46,7 +49,8 @@ class MessageSearchListView extends StatefulWidget { this.emptyBuilder, this.errorBuilder, this.separatorBuilder, - this.pullToRefresh = true, + this.itemBuilder, + this.showResultCount = true, }) : super(key: key); /// Message String to search on @@ -69,8 +73,8 @@ class MessageSearchListView extends StatefulWidget { /// message_limit: how many messages should be included to each channel final PaginationParams paginationParams; - /// Set it to false to disable the pull-to-refresh widget - final bool pullToRefresh; + /// Builder used to create a custom item preview + final MessageSearchItemBuilder itemBuilder; /// The builder used when the channel list is empty. final WidgetBuilder emptyBuilder; @@ -81,6 +85,9 @@ class MessageSearchListView extends StatefulWidget { /// 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(); } @@ -101,22 +108,7 @@ class _MessageSearchListViewState extends State { @override Widget build(BuildContext context) { final messageSearchBloc = MessageSearchBloc.of(context); - - if (!widget.pullToRefresh) { - return _buildListView(messageSearchBloc); - } - - return RefreshIndicator( - onRefresh: () async { - return messageSearchBloc.search( - filter: widget.filters, - sort: widget.sortOptions, - query: widget.messageQuery, - pagination: widget.paginationParams, - ); - }, - child: _buildListView(messageSearchBloc), - ); + return _buildListView(messageSearchBloc); } Widget _separatorBuilder(BuildContext context, int index) { @@ -129,6 +121,9 @@ class _MessageSearchListViewState extends State { } Widget _listItemBuilder(BuildContext context, Message message) { + if (widget.itemBuilder != null) { + return widget.itemBuilder(context, message); + } return MessageSearchItem(message: message); } @@ -293,7 +288,7 @@ class _MessageSearchListViewState extends State { ), ); - if (true) { + if (widget.showResultCount) { child = Column( children: [ Container( From 15669e8dd8272eb29587ebe9475ecfed605e2bce Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 14:02:21 +0530 Subject: [PATCH 08/28] [MessageSearchListView] Add itemTap property Signed-off-by: xsahil03x --- lib/src/message_search_list_view.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart index 285e452b..d5a282a4 100644 --- a/lib/src/message_search_list_view.dart +++ b/lib/src/message_search_list_view.dart @@ -7,6 +7,9 @@ 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(Message); + /// Builder used to create a custom [ListUserItem] from a [User] typedef MessageSearchItemBuilder = Widget Function(BuildContext, Message); @@ -50,6 +53,7 @@ class MessageSearchListView extends StatefulWidget { this.errorBuilder, this.separatorBuilder, this.itemBuilder, + this.onItemTap, this.showResultCount = true, }) : super(key: key); @@ -76,6 +80,9 @@ class MessageSearchListView extends StatefulWidget { /// 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; @@ -124,7 +131,10 @@ class _MessageSearchListViewState extends State { if (widget.itemBuilder != null) { return widget.itemBuilder(context, message); } - return MessageSearchItem(message: message); + return MessageSearchItem( + message: message, + onTap: () => widget.onItemTap(message), + ); } Widget _buildQueryProgressIndicator( From 1ef7511f624dbc597297201b4cac2de17283f07c Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 14:02:59 +0530 Subject: [PATCH 09/28] minor changes Signed-off-by: xsahil03x --- example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index bab4b1e0..5804ecb2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -137,6 +137,7 @@ class _ChannelListPageState extends State { } }, paginationParams: PaginationParams(limit: 20), + onItemTap: (message) {}, ) : ChannelListView( onStartChatPressed: () { From d5e8ee20713f4d0fba7f78ba969489af443da28b Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 14:08:41 +0530 Subject: [PATCH 10/28] close keyboard on listView scroll Signed-off-by: xsahil03x --- example/lib/main.dart | 62 +++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5804ecb2..3ba3e93c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -128,38 +128,42 @@ class _ChannelListPageState extends State { Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 350), - child: _isSearchActive - ? MessageSearchListView( - messageQuery: _channelQuery, - filters: { - 'members': { - r'$in': [user.id] - } - }, - paginationParams: PaginationParams(limit: 20), - onItemTap: (message) {}, - ) - : ChannelListView( - onStartChatPressed: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - swipeToAction: true, - filter: { - 'members': { - r'$in': [user.id], + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: _isSearchActive + ? MessageSearchListView( + messageQuery: _channelQuery, + filters: { + 'members': { + r'$in': [user.id] + } }, - 'draft': { - r'$ne': true, + paginationParams: PaginationParams(limit: 20), + onItemTap: (message) {}, + ) + : ChannelListView( + onStartChatPressed: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); }, - }, - options: { - 'presence': true, - }, - pagination: PaginationParams( - limit: 20, + swipeToAction: true, + filter: { + 'members': { + r'$in': [user.id], + }, + 'draft': { + r'$ne': true, + }, + }, + options: { + 'presence': true, + }, + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), ), - channelWidget: ChannelPage(), - ), + ), ), ), ], From e20ed0f012d7cc5974af961d5624526e0164b5da Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 14:09:45 +0530 Subject: [PATCH 11/28] dartfmt Signed-off-by: xsahil03x --- lib/src/stream_chat_theme.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index 9c24525c..54b096d1 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -243,10 +243,9 @@ class StreamChatThemeData { ), ), title: TextStyle( - fontSize: 14, - color: isDark ? Colors.white : Colors.black, - fontWeight: FontWeight.bold - ), + 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), From 78be96eab06944704c6c5256731a808ecc21bcaa Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 27 Nov 2020 12:49:17 +0100 Subject: [PATCH 12/28] use correct response model --- .gitignore | 3 +- example/lib/main.dart | 6 ++ example/lib/search_text_field.dart | 2 +- example/pubspec.yaml | 2 +- lib/src/channel_bottom_sheet.dart | 9 ++- lib/src/channel_name.dart | 87 ++++++++++++++------------- lib/src/message_search_bloc.dart | 16 ++--- lib/src/message_search_item.dart | 42 ++++++++++--- lib/src/message_search_list_view.dart | 18 +++--- pubspec.yaml | 2 +- 10 files changed, 114 insertions(+), 73 deletions(-) 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 3ba3e93c..26590237 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -139,6 +139,12 @@ class _ChannelListPageState extends State { r'$in': [user.id] } }, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], paginationParams: PaginationParams(limit: 20), onItemTap: (message) {}, ) 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 index bff2d68e..0128bb22 100644 --- a/lib/src/message_search_bloc.dart +++ b/lib/src/message_search_bloc.dart @@ -36,12 +36,14 @@ class MessageSearchBloc extends StatefulWidget { class MessageSearchBlocState extends State with AutomaticKeepAliveClientMixin { /// The current messages list - List get messages => _messagesController.value; + List get messageResponses => _messageResponses.value; /// The current messages list as a stream - Stream> get messagesStream => _messagesController.stream; + Stream> get messagesStream => + _messageResponses.stream; - final BehaviorSubject> _messagesController = BehaviorSubject(); + final BehaviorSubject> _messageResponses = + BehaviorSubject(); final BehaviorSubject _queryMessagesLoadingController = BehaviorSubject.seeded(false); @@ -69,7 +71,7 @@ class MessageSearchBlocState extends State pagination.offset == null || pagination.offset == 0; - final oldMessages = List.from(messages ?? []); + final oldMessages = List.from(messageResponses ?? []); final messageResponse = await client.search( filter, @@ -79,10 +81,10 @@ class MessageSearchBlocState extends State ); if (clear) { - _messagesController.add(messageResponse.results); + _messageResponses.add(messageResponse.results); } else { final temp = oldMessages + messageResponse.results; - _messagesController.add(temp); + _messageResponses.add(temp); } _queryMessagesLoadingController.add(false); @@ -99,7 +101,7 @@ class MessageSearchBlocState extends State @override void dispose() { - _messagesController.close(); + _messageResponses.close(); _queryMessagesLoadingController.close(); super.dispose(); } diff --git a/lib/src/message_search_item.dart b/lib/src/message_search_item.dart index a89f2ee0..8857fcb8 100644 --- a/lib/src/message_search_item.dart +++ b/lib/src/message_search_item.dart @@ -13,13 +13,13 @@ class MessageSearchItem extends StatelessWidget { /// Instantiate a new MessageSearchItem const MessageSearchItem({ Key key, - @required this.message, + @required this.getMessageResponse, this.onTap, this.showOnlineStatus = true, }) : super(key: key); /// [Message] displayed - final Message message; + final GetMessageResponse getMessageResponse; /// Function called when tapping this widget final VoidCallback onTap; @@ -29,9 +29,12 @@ class MessageSearchItem extends StatelessWidget { @override Widget build(BuildContext context) { - final data = Message.fromJson(message.extraData['message']); - final user = data.user; - debugPrint(message.toJson().toString()); + final message = getMessageResponse.message; + final channel = getMessageResponse.channel; + final channelName = channel.extraData['name']; + print('channel.extraData: ${channel.extraData}'); + print('channelName: ${channelName}'); + final user = message.user; return ListTile( onTap: onTap, leading: UserAvatar( @@ -42,15 +45,34 @@ class MessageSearchItem extends StatelessWidget { width: 40, ), ), - title: Text( - user.name, - style: StreamChatTheme.of(context).channelPreviewTheme.title, + title: Row( + children: [ + Text( + user.name, + style: StreamChatTheme.of(context).channelPreviewTheme.title, + ), + if (channelName != null) + Text( + ' in ', + style: StreamChatTheme.of(context) + .channelPreviewTheme + .title + .copyWith( + fontWeight: FontWeight.normal, + ), + ), + if (channelName != null) + Text( + channelName, + style: StreamChatTheme.of(context).channelPreviewTheme.title, + ), + ], ), subtitle: Row( children: [ - Expanded(child: _buildSubtitle(context, data)), + Expanded(child: _buildSubtitle(context, message)), SizedBox(width: 16), - _buildDate(context, data), + _buildDate(context, message), ], ), ); diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart index d5a282a4..8b346570 100644 --- a/lib/src/message_search_list_view.dart +++ b/lib/src/message_search_list_view.dart @@ -8,10 +8,11 @@ import 'lazy_load_scroll_view.dart'; import 'message_search_bloc.dart'; /// Callback called when tapping on a user -typedef MessageSearchItemTapCallback = void Function(Message); +typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); /// Builder used to create a custom [ListUserItem] from a [User] -typedef MessageSearchItemBuilder = Widget Function(BuildContext, Message); +typedef MessageSearchItemBuilder = Widget Function( + BuildContext, GetMessageResponse); /// /// It shows the list of searched messages. @@ -127,13 +128,14 @@ class _MessageSearchListViewState extends State { ); } - Widget _listItemBuilder(BuildContext context, Message message) { + Widget _listItemBuilder( + BuildContext context, GetMessageResponse getMessageResponse) { if (widget.itemBuilder != null) { - return widget.itemBuilder(context, message); + return widget.itemBuilder(context, getMessageResponse); } return MessageSearchItem( - message: message, - onTap: () => widget.onItemTap(message), + getMessageResponse: getMessageResponse, + onTap: () => widget.onItemTap(getMessageResponse), ); } @@ -165,7 +167,7 @@ class _MessageSearchListViewState extends State { } Widget _buildListView(MessageSearchBlocState messageSearchBloc) { - return StreamBuilder>( + return StreamBuilder>( stream: messageSearchBloc.messagesStream, builder: (context, snapshot) { if (snapshot.hasError) { @@ -276,7 +278,7 @@ class _MessageSearchListViewState extends State { filter: widget.filters, sort: widget.sortOptions, pagination: widget.paginationParams.copyWith( - offset: messageSearchBloc.messages?.length ?? 0, + offset: messageSearchBloc.messageResponses?.length ?? 0, ), query: widget.messageQuery, ), 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 From 3dd994d207107629aa12eb351fe328ef07ecc4eb Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 27 Nov 2020 12:54:29 +0100 Subject: [PATCH 13/28] fix name --- lib/src/message_search_item.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/message_search_item.dart b/lib/src/message_search_item.dart index 8857fcb8..9c7ea891 100644 --- a/lib/src/message_search_item.dart +++ b/lib/src/message_search_item.dart @@ -32,8 +32,6 @@ class MessageSearchItem extends StatelessWidget { final message = getMessageResponse.message; final channel = getMessageResponse.channel; final channelName = channel.extraData['name']; - print('channel.extraData: ${channel.extraData}'); - print('channelName: ${channelName}'); final user = message.user; return ListTile( onTap: onTap, @@ -48,7 +46,7 @@ class MessageSearchItem extends StatelessWidget { title: Row( children: [ Text( - user.name, + user.id == StreamChat.of(context).user.id ? 'You' : user.name, style: StreamChatTheme.of(context).channelPreviewTheme.title, ), if (channelName != null) From fa5b4ebab89742f38c212da362419c13134a6076 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 27 Nov 2020 17:32:08 +0530 Subject: [PATCH 14/28] minor changes Signed-off-by: xsahil03x --- lib/src/message_search_item.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/message_search_item.dart b/lib/src/message_search_item.dart index 9c7ea891..3e786b9a 100644 --- a/lib/src/message_search_item.dart +++ b/lib/src/message_search_item.dart @@ -49,7 +49,7 @@ class MessageSearchItem extends StatelessWidget { user.id == StreamChat.of(context).user.id ? 'You' : user.name, style: StreamChatTheme.of(context).channelPreviewTheme.title, ), - if (channelName != null) + if (channelName != null) ...[ Text( ' in ', style: StreamChatTheme.of(context) @@ -59,11 +59,11 @@ class MessageSearchItem extends StatelessWidget { fontWeight: FontWeight.normal, ), ), - if (channelName != null) Text( channelName, style: StreamChatTheme.of(context).channelPreviewTheme.title, ), + ], ], ), subtitle: Row( From c137ffcc6dfd18d6422108b6bbbef8c6a6b90794 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 27 Nov 2020 19:03:48 +0530 Subject: [PATCH 15/28] Add bottomNavigationBar Signed-off-by: Sahil Kumar --- example/lib/choose_user_page.dart | 6 +- example/lib/group_chat_details_screen.dart | 2 +- example/lib/main.dart | 254 +++++++++++++-------- example/lib/new_chat_screen.dart | 2 +- example/lib/routes/app_routes.dart | 6 +- example/lib/routes/routes.dart | 2 +- lib/stream_chat_flutter.dart | 1 + 7 files changed, 171 insertions(+), 102 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index 844a8c77..80b76f92 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -155,11 +155,9 @@ class ChooseUserPage extends StatelessWidget { if (!kIsWeb) { initNotifications(client); } - Navigator.pushNamedAndRemoveUntil( + Navigator.pushReplacementNamed( context, - Routes.CHANNEL_LIST, - ModalRoute.withName(Routes.CHANNEL_LIST), - arguments: client, + Routes.HOME, ); }, leading: UserAvatar( diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart index 88de33a9..d759a82e 100644 --- a/example/lib/group_chat_details_screen.dart +++ b/example/lib/group_chat_details_screen.dart @@ -132,7 +132,7 @@ class _GroupChatDetailsScreenState extends State { Navigator.pushNamedAndRemoveUntil( context, Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.CHANNEL_LIST), + ModalRoute.withName(Routes.HOME), arguments: channel, ); }, diff --git a/example/lib/main.dart b/example/lib/main.dart index 26590237..fa62878d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -60,51 +60,53 @@ class MyApp extends StatelessWidget { //TODO change to system once dark theme is implemented themeMode: ThemeMode.light, onGenerateRoute: AppRoutes.generateRoute, - initialRoute: client.state.user == null - ? Routes.CHOOSE_USER - : Routes.CHANNEL_LIST, + initialRoute: + client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, ), ); } } -class ChannelListPage extends StatefulWidget { +class HomePage extends StatefulWidget { @override - _ChannelListPageState createState() => _ChannelListPageState(); + _HomePageState createState() => _HomePageState(); } -class _ChannelListPageState extends State { - TextEditingController _controller; +class _HomePageState extends State { + int _currentIndex = 0; - String _channelQuery = ''; + bool _isSelected(int index) => _currentIndex == index; - 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(); + List get _navBarItems { + return [ + BottomNavigationBarItem( + icon: Stack( + overflow: Overflow.visible, + children: [ + StreamSvgIcon.message( + color: _isSelected(0) ? Colors.black : Colors.grey, + ), + Positioned( + top: -3, + right: -16, + child: UnreadIndicator(), + ), + ], + ), + label: 'Chats', + ), + BottomNavigationBarItem( + icon: Stack( + overflow: Overflow.visible, + children: [ + StreamSvgIcon.mentions( + color: _isSelected(1) ? Colors.black : Colors.grey, + ), + ], + ), + label: 'Mentions', + ), + ]; } @override @@ -118,63 +120,22 @@ class _ChannelListPageState extends State { ), drawer: _buildDrawer(context, user), drawerEdgeDragWidth: 50, - body: ChannelsBloc( - 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(), - ), - ), - ), - ), - ], - ), - ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _currentIndex, + items: _navBarItems, + type: BottomNavigationBarType.fixed, + selectedItemColor: Colors.black, + unselectedItemColor: Colors.grey, + onTap: (index) { + setState(() => _currentIndex = index); + }, + ), + body: IndexedStack( + index: _currentIndex, + children: [ + ChannelListPage(), + UserMentionPage(), + ], ), ); } @@ -282,6 +243,115 @@ class _ChannelListPageState extends State { } } +class UserMentionPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Text('On Pause Right Now!'), + ); + } +} + +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; + return ChannelsBloc( + 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], + }, + }, + options: { + 'presence': true, + }, + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + class ChannelQuerySearchResultPage extends StatelessWidget { final Stream> searchResultStream; diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 364c8b7e..5fbfbf07 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -338,7 +338,7 @@ class _NewChatScreenState extends State { Navigator.pushNamedAndRemoveUntil( context, Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.CHANNEL_LIST), + ModalRoute.withName(Routes.HOME), arguments: channel, ); } diff --git a/example/lib/routes/app_routes.dart b/example/lib/routes/app_routes.dart index 19352319..6b0529e1 100644 --- a/example/lib/routes/app_routes.dart +++ b/example/lib/routes/app_routes.dart @@ -13,11 +13,11 @@ class AppRoutes { static Route generateRoute(RouteSettings settings) { final args = settings.arguments; switch (settings.name) { - case Routes.CHANNEL_LIST: + case Routes.HOME: return MaterialPageRoute( - settings: const RouteSettings(name: Routes.CHANNEL_LIST), + settings: const RouteSettings(name: Routes.HOME), builder: (_) { - return ChannelListPage(); + return HomePage(); }); case Routes.CHOOSE_USER: return MaterialPageRoute( diff --git a/example/lib/routes/routes.dart b/example/lib/routes/routes.dart index f4ace9a4..0e0b1e57 100644 --- a/example/lib/routes/routes.dart +++ b/example/lib/routes/routes.dart @@ -1,6 +1,6 @@ /// Define all the route names here class Routes { - static const String CHANNEL_LIST = '/channel_list'; + static const String HOME = '/home'; static const String CHOOSE_USER = '/choose_user'; static const String ADVANCED_OPTIONS = '/advance_options'; static const String CHANNEL_PAGE = '/channel_page'; diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 35239cbd..1cbe21e4 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -41,3 +41,4 @@ export 'src/video_attachment.dart'; export 'src/message_search_bloc.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; +export 'src/unread_indicator.dart'; From 1c2f916070dab4c3e24d4f938dfdc46acb43d094 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 27 Nov 2020 15:08:29 +0100 Subject: [PATCH 16/28] fix heic pics --- example/ios/Podfile.lock | 10 ++----- example/pubspec.yaml | 2 +- lib/src/media_list_view.dart | 14 +++++++-- lib/src/message_input.dart | 57 +++++++++++++----------------------- pubspec.yaml | 1 - 5 files changed, 35 insertions(+), 49 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 707e9fcb..7552423f 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -38,7 +38,7 @@ PODS: - Firebase/Messaging (6.33.0): - Firebase/CoreOnly - FirebaseMessaging (~> 4.7.0) - - firebase_core (0.5.2): + - firebase_core (0.5.2-1): - Firebase/CoreOnly (~> 6.33.0) - Flutter - firebase_messaging (7.0.3): @@ -115,8 +115,6 @@ PODS: - nanopb/encode (1.30906.0) - path_provider (0.0.1): - Flutter - - "permission_handler (5.0.1+1)": - - Flutter - photo_manager (0.0.1): - Flutter - PromisesObjC (1.2.11) @@ -172,7 +170,6 @@ DEPENDENCIES: - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - - permission_handler (from `.symlinks/plugins/permission_handler/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) @@ -228,8 +225,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" - permission_handler: - :path: ".symlinks/plugins/permission_handler/ios" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" shared_preferences: @@ -252,7 +247,7 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 - firebase_core: 350ba329d1641211bc6183a3236893cafdacfea7 + firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781 firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75 FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 @@ -271,7 +266,6 @@ SPEC CHECKSUMS: image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c - permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6 photo_manager: f7c619c2cc8c2adb8d85c63363babac477de9c67 PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748 diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b66e085b..3291c980 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.80+82 +version: 1.0.82+84 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index 80686d62..d95c2300 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -37,7 +37,6 @@ class _MediaListViewState extends State { gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, ), - cacheExtent: 1000, itemBuilder: ( context, position, @@ -88,7 +87,7 @@ class _MediaListViewState extends State { ), ), ), - if (media.type == AssetType.video) + if (media.type == AssetType.video) ...[ Positioned( left: 8, bottom: 10, @@ -97,6 +96,17 @@ class _MediaListViewState extends State { package: 'stream_chat_flutter', ), ), + Positioned( + right: 4, + bottom: 10, + child: Text( + '${media.videoDuration.inMinutes}:${media.videoDuration.inSeconds.toString().padLeft(2, '0')}', + style: TextStyle( + color: Colors.white, + ), + ), + ), + ] ], ), onTap: () { diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 8def16da..d3522a91 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -11,7 +11,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:http_parser/http_parser.dart' as httpParser; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/compress_video_service.dart'; @@ -22,6 +21,7 @@ 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 'package:photo_manager/photo_manager.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -772,10 +772,8 @@ class MessageInputState extends State { Widget _buildPickerSection() { switch (_filePickerIndex) { case 0: - return FutureBuilder( - future: Platform.isAndroid - ? Permission.storage.status - : Permission.photos.status, + return FutureBuilder( + future: PhotoManager.requestPermission(), builder: (context, snapshot) { if (!snapshot.hasData) { return Center( @@ -783,10 +781,10 @@ class MessageInputState extends State { ); } - if (snapshot.data.isGranted) { + if (snapshot.data) { return MediaListView( selectedIds: _attachments.map((e) => e.id).toList(), - onSelect: (media) { + onSelect: (media) async { if (!_attachments .any((element) => element.id == media.id)) { _addAttachment(media); @@ -802,22 +800,7 @@ class MessageInputState extends State { return InkWell( onTap: () async { - var status = await (Platform.isAndroid - ? Permission.storage.status - : Permission.photos.status); - if (status.isPermanentlyDenied || status.isDenied) { - if (await openAppSettings()) { - setState(() {}); - } - } else { - status = await (Platform.isAndroid - ? Permission.storage - : Permission.photos) - .request(); - if (status.isGranted) { - setState(() {}); - } - } + PhotoManager.openSetting(); }, child: Container( color: Color(0xFFF2F2F2), @@ -858,7 +841,7 @@ class MessageInputState extends State { setState(() { _attachments.add(attachment); }); - final mediaFile = await medium.file; + final mediaFile = await medium.originFile; var file = PlatformFile( path: mediaFile.path, @@ -1326,7 +1309,10 @@ class MessageInputState extends State { future: VideoCompress.getFileThumbnail(attachment.file.path), builder: (context, snapshot) { if (!snapshot.hasData) { - return Offstage(); + return Image.asset( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ); } return Image.file( @@ -1405,15 +1391,6 @@ class MessageInputState extends State { _filePickerSize = _kMinMediaPickerSize; }); } else { - final status = await (Platform.isAndroid - ? Permission.storage.status - : Permission.photos.status); - if (status.isUndetermined) { - await (Platform.isAndroid - ? Permission.storage - : Permission.photos) - .request(); - } showAttachmentModal(); } }, @@ -1646,14 +1623,20 @@ class MessageInputState extends State { Future _uploadImage(PlatformFile file, Channel channel) async { final filename = file.name ?? file.path?.split('/')?.last; + httpParser.MediaType mimeType; + if (filename != null) { + if (filename.toLowerCase().endsWith('heic')) { + mimeType = httpParser.MediaType.parse('image/heic'); + } else { + mimeType = httpParser.MediaType.parse(lookupMimeType(filename)); + } + } final bytes = file.bytes; final res = await channel.sendImage( MultipartFile.fromBytes( bytes, filename: filename, - contentType: filename != null - ? httpParser.MediaType.parse(lookupMimeType(filename)) - : null, + contentType: mimeType, ), ); return res.file; diff --git a/pubspec.yaml b/pubspec.yaml index eee90a9c..75195a30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,6 @@ dependencies: carousel_slider: ^2.2.1 clipboard: ^0.1.2+8 photo_manager: ^0.5.8 - permission_handler: ^5.0.1+1 transparent_image: ^1.0.0 ezanimation: ^0.4.1 synchronized: ^2.2.0+2 From 9fb2dd5f3f310f441e9b42133650b339280012c7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 27 Nov 2020 16:06:46 +0100 Subject: [PATCH 17/28] fix video duration --- lib/src/media_list_view.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index d95c2300..507238da 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -6,6 +6,17 @@ import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'dart:ui' as ui; +extension on Duration { + String format() { + final s = '$this'.split('.')[0].padLeft(8, '0'); + if (s.startsWith('00:')) { + return s.replaceFirst('00:', ''); + } + + return s; + } +} + class MediaListView extends StatefulWidget { final List selectedIds; final void Function(AssetEntity media) onSelect; @@ -100,7 +111,7 @@ class _MediaListViewState extends State { right: 4, bottom: 10, child: Text( - '${media.videoDuration.inMinutes}:${media.videoDuration.inSeconds.toString().padLeft(2, '0')}', + media.videoDuration.format(), style: TextStyle( color: Colors.white, ), From c4647761fe351ecca5018887669782f985b64554 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 27 Nov 2020 16:30:27 +0100 Subject: [PATCH 18/28] fix channelinfo --- lib/src/channel_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/channel_info.dart b/lib/src/channel_info.dart index 208fa588..eda58f43 100644 --- a/lib/src/channel_info.dart +++ b/lib/src/channel_info.dart @@ -80,7 +80,7 @@ class ChannelInfo extends StatelessWidget { } if (!showTypingIndicator) { - return alternativeWidget; + return alternativeWidget ?? Offstage(); } return TypingIndicator( From c0d7571fbf9a912eea8d826d91362c3aacc6111f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Nov 2020 09:55:23 +0100 Subject: [PATCH 19/28] fix choose user navigation --- example/lib/choose_user_page.dart | 3 ++- example/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index 80b76f92..c728a193 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -155,9 +155,10 @@ class ChooseUserPage extends StatelessWidget { if (!kIsWeb) { initNotifications(client); } - Navigator.pushReplacementNamed( + Navigator.pushNamedAndRemoveUntil( context, Routes.HOME, + ModalRoute.withName(Routes.HOME), ); }, leading: UserAvatar( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3291c980..80a852d8 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.82+84 +version: 1.0.83+85 environment: sdk: ">=2.2.2 <3.0.0" From aed495e80f0b80bf3104e596d12f66cc14e88411 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Nov 2020 12:26:54 +0100 Subject: [PATCH 20/28] update notification --- example/lib/main.dart | 1 + example/lib/notifications_service.dart | 3 ++- example/pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index fa62878d..ff983fef 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -28,6 +28,7 @@ void main() async { showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, persistenceEnabled: true, + backgroundKeepAlive: Duration(seconds: 3), ); if (userId != null) { diff --git a/example/lib/notifications_service.dart b/example/lib/notifications_service.dart index c5b77c0e..83f5047a 100644 --- a/example/lib/notifications_service.dart +++ b/example/lib/notifications_service.dart @@ -33,7 +33,8 @@ void showLocalNotification(Message message, ChannelModel channel) async { } Future backgroundHandler(Map notification) async { - final messageId = notification['data']['message_id']; + print('new notification ${notification}'); + final messageId = notification['data']['id']; final notificationData = await NotificationService.getAndStoreMessage(messageId); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 80a852d8..5b025b4d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.83+85 +version: 1.0.84+86 environment: sdk: ">=2.2.2 <3.0.0" From 11bee325a7fedd27e337980a942a5356422d912d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Nov 2020 12:27:20 +0100 Subject: [PATCH 21/28] remove debug code --- example/lib/main.dart | 1 - example/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ff983fef..fa62878d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -28,7 +28,6 @@ void main() async { showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, persistenceEnabled: true, - backgroundKeepAlive: Duration(seconds: 3), ); if (userId != null) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5b025b4d..e84b2303 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.84+86 +version: 1.0.85+87 environment: sdk: ">=2.2.2 <3.0.0" From 6e72e8d03eeb1b04f7b2f1a2393169a49dacb344 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Nov 2020 15:41:25 +0100 Subject: [PATCH 22/28] fix mimetype of .heic pics when picked from files --- example/ios/Flutter/.last_build_id | 2 +- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 25 ++++++++++++++++--------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 20c7c514..3c74be76 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -c3e639ccf9b069e37a7d1345194e6b99 \ No newline at end of file +be54eb19d957de3aac70eae5513f67ef \ No newline at end of file diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e84b2303..5e7cc348 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.85+87 +version: 1.0.86+88 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index d3522a91..9bcddf05 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1623,14 +1623,7 @@ class MessageInputState extends State { Future _uploadImage(PlatformFile file, Channel channel) async { final filename = file.name ?? file.path?.split('/')?.last; - httpParser.MediaType mimeType; - if (filename != null) { - if (filename.toLowerCase().endsWith('heic')) { - mimeType = httpParser.MediaType.parse('image/heic'); - } else { - mimeType = httpParser.MediaType.parse(lookupMimeType(filename)); - } - } + final mimeType = _getMimeType(filename); final bytes = file.bytes; final res = await channel.sendImage( MultipartFile.fromBytes( @@ -1642,14 +1635,28 @@ class MessageInputState extends State { return res.file; } + httpParser.MediaType _getMimeType(String filename) { + httpParser.MediaType mimeType; + if (filename != null) { + if (filename.toLowerCase().endsWith('heic')) { + mimeType = httpParser.MediaType.parse('image/heic'); + } else { + mimeType = httpParser.MediaType.parse(lookupMimeType(filename)); + } + } + + return mimeType; + } + Future _uploadFile(PlatformFile file, Channel channel) async { final filename = file.name ?? file.path?.split('/')?.last; + final mimeType = _getMimeType(filename); final bytes = file.bytes; final res = await channel.sendFile( MultipartFile.fromBytes( bytes, filename: filename, - contentType: httpParser.MediaType.parse(lookupMimeType(filename)), + contentType: mimeType, ), ); return res.file; From 48738c84c5b8411f2f55b65cf7aaa74a6e9b478b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Nov 2020 17:17:35 +0100 Subject: [PATCH 23/28] fix compressed video upload --- lib/src/message_input.dart | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 9bcddf05..777ef1b3 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1549,8 +1549,23 @@ class MessageInputState extends State { return; } + final channel = StreamChannel.of(context).channel; + final attachment = _SendingAttachment( + file: file, + attachment: Attachment( + localUri: file.path != null ? Uri.parse(file.path) : null, + type: attachmentType, + ), + ); + + setState(() { + _attachments.add(attachment); + }); + + final mimeType = _getMimeType(file.name); + if (file.size > _kMaxAttachmentSize) { - if (attachmentType == 'video') { + if (attachmentType == 'video' || mimeType?.type == 'video') { final mediaInfo = await CompressVideoService.compressVideo(file.path); file = PlatformFile( name: mediaInfo.title, @@ -1566,22 +1581,13 @@ class MessageInputState extends State { ), ), ); + setState(() { + _attachments.remove(attachment); + }); + return; } } - final channel = StreamChannel.of(context).channel; - final attachment = _SendingAttachment( - file: file, - attachment: Attachment( - localUri: file.path != null ? Uri.parse(file.path) : null, - type: attachmentType, - ), - ); - - setState(() { - _attachments.add(attachment); - }); - final url = await _uploadAttachment(file, fileType, channel); if (fileType == DefaultAttachmentTypes.image) { @@ -1622,7 +1628,7 @@ class MessageInputState extends State { } Future _uploadImage(PlatformFile file, Channel channel) async { - final filename = file.name ?? file.path?.split('/')?.last; + final filename = file.path?.split('/')?.last; final mimeType = _getMimeType(filename); final bytes = file.bytes; final res = await channel.sendImage( @@ -1649,7 +1655,7 @@ class MessageInputState extends State { } Future _uploadFile(PlatformFile file, Channel channel) async { - final filename = file.name ?? file.path?.split('/')?.last; + final filename = file.path?.split('/')?.last; final mimeType = _getMimeType(filename); final bytes = file.bytes; final res = await channel.sendFile( From 46dcf6a629c8d3e9d595b189dd2e13c6bf5a667f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Nov 2020 18:07:11 +0100 Subject: [PATCH 24/28] fix compression video --- lib/src/message_input.dart | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 777ef1b3..6714e812 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -869,7 +869,7 @@ class MessageInputState extends State { } file = PlatformFile( name: file.name, - size: mediaInfo.filesize, + size: (mediaInfo.filesize / 1024).ceil(), bytes: await mediaInfo.file.readAsBytes(), path: mediaInfo.path, ); @@ -1294,6 +1294,12 @@ class MessageInputState extends State { ? Image.memory( attachment.file.bytes, fit: BoxFit.cover, + errorBuilder: (context, _, __) { + return Image.asset( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ); + }, ) : Image.network( attachment.attachment.imageUrl, @@ -1520,6 +1526,7 @@ class MessageInputState extends State { } final bytes = await pickedFile.readAsBytes(); file = PlatformFile( + size: (bytes.length / 1024).ceil(), path: pickedFile.path, bytes: bytes, ); @@ -1549,6 +1556,12 @@ class MessageInputState extends State { return; } + final mimeType = _getMimeType(file.name); + + if (mimeType.type == 'video' || mimeType.type == 'image') { + attachmentType = mimeType.type; + } + final channel = StreamChannel.of(context).channel; final attachment = _SendingAttachment( file: file, @@ -1562,17 +1575,18 @@ class MessageInputState extends State { _attachments.add(attachment); }); - final mimeType = _getMimeType(file.name); - if (file.size > _kMaxAttachmentSize) { - if (attachmentType == 'video' || mimeType?.type == 'video') { + if (attachmentType == 'video') { final mediaInfo = await CompressVideoService.compressVideo(file.path); file = PlatformFile( name: mediaInfo.title, - size: mediaInfo.filesize, + size: (mediaInfo.filesize / 1024).ceil(), bytes: await mediaInfo.file.readAsBytes(), path: mediaInfo.path, ); + setState(() { + attachment.file = file; + }); } else { Scaffold.of(context).showSnackBar( SnackBar( From 7f0c8fe25a54ca0d000ad2841c53f105e8de6970 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Nov 2020 18:10:55 +0100 Subject: [PATCH 25/28] fix mimetype --- lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6714e812..27f9f7bd 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1556,7 +1556,7 @@ class MessageInputState extends State { return; } - final mimeType = _getMimeType(file.name); + final mimeType = _getMimeType(file.path.split('/').last); if (mimeType.type == 'video' || mimeType.type == 'image') { attachmentType = mimeType.type; From 9659008ae868d9880067af81ce80c0a525e4d3f4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Dec 2020 11:14:21 +0100 Subject: [PATCH 26/28] remove close button if search is not active --- example/ios/Flutter/.last_build_id | 2 +- example/lib/main.dart | 1 + example/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 3c74be76..20c7c514 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -be54eb19d957de3aac70eae5513f67ef \ No newline at end of file +c3e639ccf9b069e37a7d1345194e6b99 \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index fa62878d..4b529e23 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -300,6 +300,7 @@ class _ChannelListPageState extends State { children: [ SearchTextField( controller: _controller, + showCloseButton: _isSearchActive, ), Expanded( child: AnimatedSwitcher( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5e7cc348..6ae2ab54 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.86+88 +version: 1.0.87+89 environment: sdk: ">=2.2.2 <3.0.0" From 1e8200a86a6a941254a8c5f7f17531d87ca8ea61 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Dec 2020 15:17:10 +0100 Subject: [PATCH 27/28] fix deleted message borderradius --- lib/src/deleted_message.dart | 63 ++++++++++++++++++++++-------------- lib/src/message_widget.dart | 1 + 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/lib/src/deleted_message.dart b/lib/src/deleted_message.dart index 673d79c8..9181d985 100644 --- a/lib/src/deleted_message.dart +++ b/lib/src/deleted_message.dart @@ -1,6 +1,7 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; class DeletedMessage extends StatelessWidget { const DeletedMessage({ @@ -9,6 +10,7 @@ class DeletedMessage extends StatelessWidget { this.borderRadiusGeometry, this.shape, this.borderSide, + this.reverse = false, }) : super(key: key); /// The theme of the message @@ -23,33 +25,44 @@ class DeletedMessage extends StatelessWidget { /// The borderside of the message text final BorderSide borderSide; + /// If true the widget will be mirrored + final bool reverse; + @override Widget build(BuildContext context) { - return Material( - color: messageTheme.messageBackgroundColor, - shape: shape ?? - RoundedRectangleBorder( - borderRadius: borderRadiusGeometry ?? BorderRadius.zero, - side: borderSide ?? - BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withAlpha(24) - : Colors.black.withAlpha(24), - ), + return Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: Material( + color: messageTheme.messageBackgroundColor, + shape: shape ?? + RoundedRectangleBorder( + borderRadius: borderRadiusGeometry ?? BorderRadius.zero, + side: borderSide ?? + BorderSide( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withAlpha(24) + : Colors.black.withAlpha(24), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16, ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16, - ), - child: Text( - 'Message deleted', - style: messageTheme.messageText.copyWith( - fontStyle: FontStyle.italic, - color: (Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black) - .withOpacity(.5), + child: Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: Text( + 'Message deleted', + style: messageTheme.messageText.copyWith( + fontStyle: FontStyle.italic, + color: (Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black) + .withOpacity(.5), + ), + ), ), ), ), diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 45f3c62e..88a93d6f 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -286,6 +286,7 @@ class _MessageWidgetState extends State { transform: Matrix4.rotationY( widget.reverse ? pi : 0), child: DeletedMessage( + reverse: widget.reverse, borderRadiusGeometry: widget.borderRadiusGeometry, borderSide: widget.borderSide, From ac0fc427aeaea0b7f0a0c46ab11d3002e8e1fff4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Dec 2020 15:18:48 +0100 Subject: [PATCH 28/28] fix messageinput send icon --- lib/src/message_input.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 27f9f7bd..1dbc45df 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1719,17 +1719,17 @@ class MessageInputState extends State { if (_commandEnabled) { return 'Icon_search.svg'; } else { - return 'Icon_circle_up.svg'; + return 'Icon_circle_right.svg'; } } String _getSendIcon() { if (widget.editMessage != null) { - return 'Icon_circle_right.svg'; + return 'Icon_circle_up.svg'; } else if (_commandEnabled) { return 'Icon_search.svg'; } else { - return 'Icon_circle_right.svg'; + return 'Icon_circle_up.svg'; } }