diff --git a/packages/chatty/lib/ui/common/my_channel_preview.dart b/packages/chatty/lib/ui/common/my_channel_preview.dart index 9156d83..9d54a9b 100644 --- a/packages/chatty/lib/ui/common/my_channel_preview.dart +++ b/packages/chatty/lib/ui/common/my_channel_preview.dart @@ -1,9 +1,6 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /*Navigator.push( context, diff --git a/packages/imessage/lib/message_page.dart b/packages/imessage/lib/message_page.dart index 23d15df..497e19c 100644 --- a/packages/imessage/lib/message_page.dart +++ b/packages/imessage/lib/message_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:imessage/message_list_view.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' show diff --git a/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj index 3b333d0..5b28f53 100644 --- a/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -177,7 +177,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1140; - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -245,7 +245,7 @@ "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", - "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework", + "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", @@ -256,16 +256,16 @@ "${BUILT_PRODUCTS_DIR}/image_gallery_saver/image_gallery_saver.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", - "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", - "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", "${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework", - "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", + "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", "${BUILT_PRODUCTS_DIR}/video_compress/video_compress.framework", - "${BUILT_PRODUCTS_DIR}/video_player/video_player.framework", + "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework", "${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework", "${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework", ); @@ -285,16 +285,16 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_gallery_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_compress.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework", ); @@ -442,7 +442,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -581,7 +584,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -615,7 +621,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/packages/stream_chat_v1/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_v1/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fb2dffc..c87d15a 100644 --- a/packages/stream_chat_v1/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/stream_chat_v1/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ ? sortOptions; - - /// Pagination parameters - /// limit: the number of users to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - final PaginationParams paginationParams; - - /// The builder used when the file list is empty. - final WidgetBuilder? emptyBuilder; + final StreamMessageThemeData messageTheme; const ChannelFileDisplayScreen({ - this.sortOptions, - this.paginationParams = const PaginationParams(limit: 20), - this.emptyBuilder, - }); + Key? key, + required this.messageTheme, + }) : super(key: key); @override - _ChannelFileDisplayScreenState createState() => + State createState() => _ChannelFileDisplayScreenState(); } class _ChannelFileDisplayScreenState extends State { - @override - void initState() { - super.initState(); - final messageSearchBloc = MessageSearchBloc.of(context); - messageSearchBloc.search( - filter: Filter.in_( - 'cid', - [StreamChannel.of(context).channel.cid!], + final Map controllerCache = {}; + + late final controller = StreamMessageSearchListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.in_( + 'attachments.type', + ['file'], + ), + sort: [ + SortOption( + 'created_at', + direction: SortOption.ASC, ), - messageFilter: Filter.in_( - 'attachments.type', - ['file'], - ), - sort: widget.sortOptions, - pagination: widget.paginationParams, - ); - } + ], + limit: 20, + ); @override Widget build(BuildContext context) { @@ -58,120 +48,109 @@ class _ChannelFileDisplayScreenState extends State { title: Text( AppLocalizations.of(context).files, style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, - fontSize: 16.0), - ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - width: 24.0, - height: 24.0, - child: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, - size: 24.0, - ), - ), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + fontSize: 16.0, ), ), + leading: StreamBackButton(), backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, ), - body: _buildMediaGrid(), - ); - } - - Widget _buildMediaGrid() { - final messageSearchBloc = MessageSearchBloc.of(context); - - return StreamBuilder>( - builder: (context, snapshot) { - if (snapshot.data == null) { - return Center( - child: const CircularProgressIndicator(), - ); - } - - if (snapshot.data!.isEmpty) { - if (widget.emptyBuilder != null) { - return widget.emptyBuilder!(context); - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon.files( - size: 136.0, - color: StreamChatTheme.of(context).colorTheme.disabled, - ), - SizedBox(height: 16.0), - Text( - AppLocalizations.of(context).noFiles, - style: TextStyle( - fontSize: 14.0, - color: - StreamChatTheme.of(context).colorTheme.textHighEmphasis, + body: ValueListenableBuilder( + valueListenable: controller, + builder: ( + BuildContext context, + PagedValue value, + Widget? child, + ) { + return value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.files( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + SizedBox(height: 16.0), + Text( + AppLocalizations.of(context).noFiles, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, + ), + ), + SizedBox(height: 8.0), + Text( + AppLocalizations.of(context).filesAppearHere, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + ], ), - ), - SizedBox(height: 8.0), - Text( - AppLocalizations.of(context).filesAppearHere, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.0, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - ), - ), - ], - ), - ); - } + ); + } + final media = {}; - final media = {}; + for (var item in items) { + item.message.attachments + .where((e) => e.type == 'file') + .forEach((e) { + media[e] = item.message; + }); + } - for (var item in snapshot.data!) { - item.message.attachments.where((e) => e.type == 'file').forEach((e) { - media[e] = item.message; - }); - } - - return LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( - filter: Filter.in_( - 'cid', - [StreamChannel.of(context).channel.cid!], - ), - messageFilter: Filter.in_( - 'attachments.type', - ['file'], - ), - sort: widget.sortOptions, - pagination: widget.paginationParams.copyWith( - offset: messageSearchBloc.messageResponses?.length ?? 0, - ), - ), - child: ListView.builder( - itemBuilder: (context, position) { - return Padding( - padding: const EdgeInsets.all(1.0), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: FileAttachment( - message: media.values.toList()[position], - attachment: media.keys.toList()[position], - ), + return LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) { + controller.loadMore(nextPageKey); + } + }, + child: ListView.builder( + itemBuilder: (context, position) { + return Padding( + padding: const EdgeInsets.all(1.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: StreamFileAttachment( + message: media.values.toList()[position], + attachment: media.keys.toList()[position], + ), + ), + ); + }, + itemCount: media.length, ), ); }, - itemCount: media.length, - ), - ); - }, - stream: messageSearchBloc.messagesStream, + loading: () => Center( + child: const CircularProgressIndicator(), + ), + error: (_) => Offstage(), + ); + }, + ), ); } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + controller.doInitialLoad(); + super.initState(); + } } diff --git a/packages/stream_chat_v1/lib/channel_list.dart b/packages/stream_chat_v1/lib/channel_list.dart index d13e830..dcc1e91 100644 --- a/packages/stream_chat_v1/lib/channel_list.dart +++ b/packages/stream_chat_v1/lib/channel_list.dart @@ -6,6 +6,7 @@ import 'package:example/search_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'channel_page.dart'; import 'chat_info_screen.dart'; @@ -19,9 +20,21 @@ class ChannelList extends StatefulWidget { class _ChannelList extends State { ScrollController _scrollController = ScrollController(); - TextEditingController? _controller; + late StreamMessageSearchListController _messageSearchListController = + StreamMessageSearchListController( + client: StreamChat.of(context).client, + filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]), + limit: 5, + searchQuery: '', + sort: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + ); - String _channelQuery = ''; + TextEditingController? _controller; bool _isSearchActive = false; @@ -31,14 +44,25 @@ class _ChannelList extends State { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted) { + _messageSearchListController.searchQuery = _controller!.text; setState(() { - _channelQuery = _controller!.text; - _isSearchActive = _channelQuery.isNotEmpty; + _isSearchActive = _controller!.text.isNotEmpty; }); + if (_isSearchActive) _messageSearchListController.doInitialLoad(); } }); } + late final _channelListController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + presence: true, + limit: 30, + ); + @override void initState() { super.initState(); @@ -50,12 +74,12 @@ class _ChannelList extends State { _controller?.removeListener(_channelQueryListener); _controller?.dispose(); _scrollController.dispose(); + _channelListController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final user = StreamChat.of(context).currentUser; return WillPopScope( onWillPop: () async { if (_isSearchActive) { @@ -86,18 +110,8 @@ class _ChannelList extends State { ), ], body: _isSearchActive - ? MessageSearchListView( - showErrorTile: true, - messageQuery: _channelQuery, - filters: Filter.in_('members', [user!.id]), - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - pullToRefresh: false, - limit: 30, + ? StreamMessageSearchListView( + controller: _messageSearchListController, emptyBuilder: (_) { return LayoutBuilder( builder: (context, viewportConstraints) { @@ -128,80 +142,175 @@ class _ChannelList extends State { }, ); }, - onItemTap: (messageResponse) async { - FocusScope.of(context).requestFocus(FocusNode()); - 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, - ), + itemBuilder: ( + context, + messageResponses, + index, + defaultWidget, + ) { + return defaultWidget.copyWith( + onTap: () async { + final messageResponse = messageResponses[index]; + FocusScope.of(context).requestFocus(FocusNode()); + 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, + ), + ); + }, ); }, ) - : ChannelListView( - onChannelTap: (channel, _) { - Navigator.pushNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - ), - ); - }, - onStartChatPressed: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - swipeToAction: true, - filter: Filter.in_('members', [user!.id]), - presence: true, - limit: 30, - onViewInfoTap: (channel) { - Navigator.pop(context); - if (channel.memberCount == 2 && channel.isDistinct) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( + : SlidableAutoCloseBehavior( + closeWhenOpened: true, + child: RefreshIndicator( + onRefresh: _channelListController.refresh, + child: StreamChannelListView( + controller: _channelListController, + itemBuilder: (context, channels, index, defaultWidget) { + final chatTheme = StreamChatTheme.of(context); + final backgroundColor = chatTheme.colorTheme.inputBg; + final channel = channels[index]; + final canDeleteChannel = channel.ownCapabilities + .contains(PermissionType.deleteChannel); + return Slidable( + groupTag: 'channels-actions', + endActionPane: ActionPane( + extentRatio: canDeleteChannel ? 0.40 : 0.20, + motion: const BehindMotion(), + children: [ + CustomSlidableAction( + child: Icon(Icons.more_horiz), + backgroundColor: backgroundColor, + onPressed: (_) { + showChannelInfoModalBottomSheet( + context: context, + channel: channel, + onViewInfoTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + final isOneToOne = + channel.memberCount == 2 && + channel.isDistinct; + return StreamChannel( + channel: channel, + child: isOneToOne + ? ChatInfoScreen( + messageTheme: chatTheme + .ownMessageTheme, + user: channel + .state!.members + .where((m) => + m.userId != + channel + .client + .state + .currentUser! + .id) + .first + .user, + ) + : GroupInfoScreen( + messageTheme: chatTheme + .ownMessageTheme, + ), + ); + }, + ), + ); + }, + ); + }, + ), + if (canDeleteChannel) + CustomSlidableAction( + backgroundColor: backgroundColor, + child: StreamSvgIcon.delete( + color: chatTheme.colorTheme.accentError, + ), + onPressed: (_) async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + question: + 'Are you sure you want to delete this conversation?', + okText: 'Delete', + cancelText: 'Cancel', + icon: StreamSvgIcon.delete( + color: chatTheme.colorTheme.accentError, + ), + ); + if (res == true) { + await _channelListController + .deleteChannel(channel); + } + }, + ), + ], + ), + child: defaultWidget, + ); + }, + onChannelTap: (channel) { + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( channel: channel, - child: ChatInfoScreen( - messageTheme: - StreamChatTheme.of(context).ownMessageTheme, - user: channel.state!.members - .where((m) => - m.userId != - channel.client.state.currentUser!.id) - .first - .user, + ), + ); + }, + emptyBuilder: (_) { + return Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.message( + size: 148, + color: StreamChatTheme.of(context) + .colorTheme + .disabled, + ), + emptyTitle: TextButton( + onPressed: () { + Navigator.pushNamed( + context, + Routes.NEW_CHAT, + ); + }, + child: Text( + 'Start a chat', + style: StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .accentPrimary, + ), + ), + ), ), ), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: GroupInfoScreen( - messageTheme: - StreamChatTheme.of(context).ownMessageTheme, - ), - ), - ), - ); - } - }, + ); + }, + ), + ), ), ), ), diff --git a/packages/stream_chat_v1/lib/channel_list_page.dart b/packages/stream_chat_v1/lib/channel_list_page.dart index be43bd6..6a9438f 100644 --- a/packages/stream_chat_v1/lib/channel_list_page.dart +++ b/packages/stream_chat_v1/lib/channel_list_page.dart @@ -40,7 +40,7 @@ class _ChannelListPageState extends State { Positioned( top: -3, right: -16, - child: UnreadIndicator(), + child: StreamUnreadIndicator(), ), ], ), @@ -70,7 +70,7 @@ class _ChannelListPageState extends State { } return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, - appBar: ChannelListHeader( + appBar: StreamChannelListHeader( onNewChatButtonTap: () { Navigator.pushNamed(context, Routes.NEW_CHAT); }, @@ -100,11 +100,7 @@ class _ChannelListPageState extends State { body: IndexedStack( index: _currentIndex, children: [ - ChannelsBloc( - child: MessageSearchBloc( - child: ChannelList(), - ), - ), + ChannelList(), UserMentionsPage(), ], ), @@ -165,7 +161,7 @@ class LeftDrawer extends StatelessWidget { ), child: Row( children: [ - UserAvatar( + StreamUserAvatar( user: user, showOnlineStatus: false, constraints: BoxConstraints.tight(Size.fromRadius(20)), diff --git a/packages/stream_chat_v1/lib/channel_media_display_screen.dart b/packages/stream_chat_v1/lib/channel_media_display_screen.dart index ee23293..65c9a5d 100644 --- a/packages/stream_chat_v1/lib/channel_media_display_screen.dart +++ b/packages/stream_chat_v1/lib/channel_media_display_screen.dart @@ -1,61 +1,45 @@ import 'package:example/localizations.dart'; +import 'package:example/routes/routes.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; +import 'channel_page.dart'; + class ChannelMediaDisplayScreen extends StatefulWidget { - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. - /// Direction can be ascending or descending. - final List? sortOptions; - - /// Pagination parameters - /// limit: the number of users to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - final PaginationParams paginationParams; - - /// The builder used when the file list is empty. - final WidgetBuilder? emptyBuilder; - - final ShowMessageCallback? onShowMessage; - - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; const ChannelMediaDisplayScreen({ + Key? key, required this.messageTheme, - this.sortOptions, - this.paginationParams = const PaginationParams(limit: 20), - this.emptyBuilder, - this.onShowMessage, - }); + }) : super(key: key); @override - _ChannelMediaDisplayScreenState createState() => + State createState() => _ChannelMediaDisplayScreenState(); } class _ChannelMediaDisplayScreenState extends State { - Map controllerCache = {}; + final Map controllerCache = {}; - @override - void initState() { - super.initState(); - final messageSearchBloc = MessageSearchBloc.of(context); - messageSearchBloc.search( - filter: Filter.in_( - 'cid', - [StreamChannel.of(context).channel.cid!], + late final controller = StreamMessageSearchListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.in_( + 'attachments.type', + ['image', 'video'], + ), + sort: [ + SortOption( + 'created_at', + direction: SortOption.ASC, ), - messageFilter: Filter.in_( - 'attachments.type', - ['image', 'video'], - ), - sort: widget.sortOptions, - pagination: widget.paginationParams, - ); - } + ], + limit: 20, + ); @override Widget build(BuildContext context) { @@ -74,155 +58,169 @@ class _ChannelMediaDisplayScreenState extends State { leading: StreamBackButton(), backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, ), - body: _buildMediaGrid(), - ); - } - - Widget _buildMediaGrid() { - final messageSearchBloc = MessageSearchBloc.of(context); - - return StreamBuilder>( - builder: (context, snapshot) { - if (snapshot.data == null) { - return Center( - child: const CircularProgressIndicator(), - ); - } - - if (snapshot.data!.isEmpty) { - if (widget.emptyBuilder != null) { - return widget.emptyBuilder!(context); - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon.pictures( - size: 136.0, - color: StreamChatTheme.of(context).colorTheme.disabled, - ), - SizedBox(height: 16.0), - Text( - AppLocalizations.of(context).noMedia, - style: TextStyle( - fontSize: 14.0, - color: - StreamChatTheme.of(context).colorTheme.textHighEmphasis, - ), - ), - SizedBox(height: 8.0), - Text( - AppLocalizations.of(context).photosOrVideosWillAppearHere, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.0, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - ), - ), - ], - ), - ); - } - - final media = <_AssetPackage>[]; - - for (var item in snapshot.data!) { - item.message.attachments - .where((e) => - (e.type == 'image' || e.type == 'video') && - e.ogScrapeUrl == null) - .forEach((e) { - VideoPlayerController? controller; - if (e.type == 'video') { - var cachedController = controllerCache[e.assetUrl]; - - if (cachedController == null) { - controller = VideoPlayerController.network(e.assetUrl!); - controller.initialize(); - controllerCache[e.assetUrl] = controller; - } else { - controller = cachedController; - } - } - media.add(_AssetPackage(e, item.message, controller)); - }); - } - - return LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( - filter: Filter.in_( - 'cid', - [StreamChannel.of(context).channel.cid!], - ), - messageFilter: Filter.in_( - 'attachments.type', - ['image', 'video'], - ), - sort: widget.sortOptions, - pagination: widget.paginationParams.copyWith( - offset: messageSearchBloc.messageResponses?.length ?? 0, - ), - ), - child: GridView.builder( - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), - itemBuilder: (context, position) { - var channel = StreamChannel.of(context).channel; - return Padding( - padding: const EdgeInsets.all(1.0), - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: FullScreenMedia( - mediaAttachments: - media.map((e) => e.attachment).toList(), - startIndex: position, - message: media[position].message, - userName: media[position].message.user!.name, - onShowMessage: widget.onShowMessage, - ), + body: ValueListenableBuilder( + valueListenable: controller, + builder: (BuildContext context, + PagedValue value, Widget? child) { + return value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.pictures( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + SizedBox(height: 16.0), + Text( + AppLocalizations.of(context).noMedia, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis, ), ), + SizedBox(height: 8.0), + Text( + AppLocalizations.of(context) + .photosOrVideosWillAppearHere, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + final media = <_AssetPackage>[]; + + for (var item in value.asSuccess.items) { + item.message.attachments + .where((e) => + (e.type == 'image' || e.type == 'video') && + e.ogScrapeUrl == null) + .forEach((e) { + VideoPlayerController? controller; + if (e.type == 'video') { + var cachedController = controllerCache[e.assetUrl]; + + if (cachedController == null) { + controller = VideoPlayerController.network(e.assetUrl!); + controller.initialize(); + controllerCache[e.assetUrl] = controller; + } else { + controller = cachedController; + } + } + media.add(_AssetPackage(e, item.message, controller)); + }); + } + + return LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) { + controller.loadMore(nextPageKey); + } + }, + child: GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3), + itemBuilder: (context, position) { + var channel = StreamChannel.of(context).channel; + return Padding( + padding: const EdgeInsets.all(1.0), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: StreamFullScreenMedia( + mediaAttachmentPackages: media + .map( + (e) => StreamAttachmentPackage( + attachment: e.attachment, + message: e.message, + ), + ) + .toList(), + startIndex: position, + userName: media[position].message.user!.name, + onShowMessage: (m, c) async { + final client = + StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ), + ), + ); + }, + child: media[position].attachment.type == 'image' + ? IgnorePointer( + child: StreamImageAttachment( + attachment: media[position].attachment, + message: media[position].message, + showTitle: false, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + messageTheme: widget.messageTheme, + ), + ) + : VideoPlayer(media[position].videoPlayer!), + ), ); }, - child: media[position].attachment.type == 'image' - ? IgnorePointer( - child: ImageAttachment( - attachment: media[position].attachment, - message: media[position].message, - showTitle: false, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), - messageTheme: widget.messageTheme, - ), - ) - : VideoPlayer(media[position].videoPlayer!), + itemCount: media.length, ), ); }, - itemCount: media.length, - ), - ); - }, - stream: messageSearchBloc.messagesStream, + loading: () => Center( + child: const CircularProgressIndicator(), + ), + error: (_) => Offstage(), + ); + }, + ), ); } @override void dispose() { + controller.dispose(); super.dispose(); - for (var c in controllerCache.values) { - c!.dispose(); - } + } + + @override + void initState() { + controller.doInitialLoad(); + super.initState(); } } diff --git a/packages/stream_chat_v1/lib/channel_page.dart b/packages/stream_chat_v1/lib/channel_page.dart index 6ed2398..e2b2d42 100644 --- a/packages/stream_chat_v1/lib/channel_page.dart +++ b/packages/stream_chat_v1/lib/channel_page.dart @@ -34,8 +34,8 @@ class ChannelPage extends StatefulWidget { } class _ChannelPageState extends State { - Message? _quotedMessage; FocusNode? _focusNode; + StreamMessageInputController _messageInputController = StreamMessageInputController(); @override void initState() { @@ -50,7 +50,7 @@ class _ChannelPageState extends State { } void _reply(Message message) { - setState(() => _quotedMessage = message); + _messageInputController.quotedMessage = message; WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { _focusNode!.requestFocus(); }); @@ -60,7 +60,7 @@ class _ChannelPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, - appBar: ChannelHeader( + appBar: StreamChannelHeader( showTypingIndicator: false, onImageTap: () async { var channel = StreamChannel.of(context).channel; @@ -108,7 +108,7 @@ class _ChannelPageState extends State { Expanded( child: Stack( children: [ - MessageListView( + StreamMessageListView( initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, highlightInitialMessage: widget.highlightInitialMessage, @@ -137,14 +137,13 @@ class _ChannelPageState extends State { ); }, deletedBottomRowBuilder: (context, message) { - return const VisibleFootnote(); + return const StreamVisibleFootnote(); }, ); }, threadBuilder: (_, parentMessage) { return ThreadPage(parent: parentMessage!); }, - pinPermissions: ['owner', 'admin', 'member'], ), Positioned( bottom: 0, @@ -156,8 +155,7 @@ class _ChannelPageState extends State { .colorTheme .appBg .withOpacity(.9), - child: TypingIndicator( - alignment: Alignment.centerLeft, + child: StreamTypingIndicator( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, @@ -175,13 +173,9 @@ class _ChannelPageState extends State { ], ), ), - MessageInput( + StreamMessageInput( focusNode: _focusNode, - quotedMessage: _quotedMessage, - onQuotedMessageCleared: () { - setState(() => _quotedMessage = null); - _focusNode!.unfocus(); - }, + messageInputController: _messageInputController, ), ], ), diff --git a/packages/stream_chat_v1/lib/chat_info_screen.dart b/packages/stream_chat_v1/lib/chat_info_screen.dart index c6b6a8c..731050d 100644 --- a/packages/stream_chat_v1/lib/chat_info_screen.dart +++ b/packages/stream_chat_v1/lib/chat_info_screen.dart @@ -1,22 +1,18 @@ -import 'package:collection/collection.dart' show IterableExtension; import 'package:example/localizations.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_file_display_screen.dart'; import 'channel_media_display_screen.dart'; -import 'channel_page.dart'; import 'pinned_messages_screen.dart'; -import 'routes/routes.dart'; /// Detail screen for a 1:1 chat correspondence class ChatInfoScreen extends StatefulWidget { /// User in consideration final User? user; - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; const ChatInfoScreen({ Key? key, @@ -54,13 +50,7 @@ class _ChatInfoScreenState extends State { height: 8.0, color: StreamChatTheme.of(context).colorTheme.disabled, ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.currentUser!.id) - ?.role)) + if (channel.ownCapabilities.contains(PermissionType.deleteChannel)) _buildDeleteListTile(), ], ), @@ -78,7 +68,7 @@ class _ChatInfoScreenState extends State { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: UserAvatar( + child: StreamUserAvatar( user: widget.user!, constraints: BoxConstraints.tightFor( width: 72.0, @@ -95,7 +85,7 @@ class _ChatInfoScreenState extends State { SizedBox(height: 7.0), _buildConnectedTitleState(), SizedBox(height: 15.0), - OptionListTile( + StreamOptionListTile( title: '@${widget.user!.id}', tileColor: StreamChatTheme.of(context).colorTheme.appBg, trailing: Padding( @@ -148,7 +138,7 @@ class _ChatInfoScreenState extends State { builder: (context, snapshot) { mutedBool.value = snapshot.data; - return OptionListTile( + return StreamOptionListTile( tileColor: StreamChatTheme.of(context).colorTheme.appBg, title: AppLocalizations.of(context).muteUser, titleTextStyle: StreamChatTheme.of(context).textTheme.body, @@ -201,7 +191,7 @@ class _ChatInfoScreenState extends State { // ), // onTap: () {}, // ), - OptionListTile( + StreamOptionListTile( title: AppLocalizations.of(context).pinnedMessages, tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, @@ -226,43 +216,13 @@ class _ChatInfoScreenState extends State { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: MessageSearchBloc( - child: PinnedMessagesScreen( - messageTheme: widget.messageTheme, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - onShowMessage: (m, c) async { - final client = StreamChat.of(context).client; - final message = m; - final channel = client.channel( - c.type, - id: c.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushNamedAndRemoveUntil( - context, - Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.CHANNEL_LIST_PAGE), - arguments: ChannelPageArgs( - channel: c, - initialMessage: m, - ), - ); - }, - ), - ), + child: PinnedMessagesScreen(), ), ), ); }, ), - OptionListTile( + StreamOptionListTile( title: AppLocalizations.of(context).photosAndVideos, tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, @@ -287,42 +247,15 @@ class _ChatInfoScreenState extends State { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: MessageSearchBloc( - child: ChannelMediaDisplayScreen( - messageTheme: widget.messageTheme, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - onShowMessage: (m, c) async { - final client = StreamChat.of(context).client; - final message = m; - final channel = client.channel( - c.type, - id: c.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ), + child: ChannelMediaDisplayScreen( + messageTheme: widget.messageTheme, ), ), ), ); }, ), - OptionListTile( + StreamOptionListTile( title: AppLocalizations.of(context).files, tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, @@ -347,22 +280,15 @@ class _ChatInfoScreenState extends State { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: MessageSearchBloc( - child: ChannelFileDisplayScreen( - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - ), + child: ChannelFileDisplayScreen( + messageTheme: widget.messageTheme, ), ), ), ); }, ), - OptionListTile( + StreamOptionListTile( title: AppLocalizations.of(context).sharedGroups, tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, @@ -392,7 +318,7 @@ class _ChatInfoScreenState extends State { } Widget _buildDeleteListTile() { - return OptionListTile( + return StreamOptionListTile( title: 'Delete Conversation', tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith( @@ -633,7 +559,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { children: [ Padding( padding: const EdgeInsets.all(8.0), - child: ChannelAvatar( + child: StreamChannelAvatar( channel: channel, constraints: BoxConstraints(maxWidth: 40.0, maxHeight: 40.0), diff --git a/packages/stream_chat_v1/lib/choose_user_page.dart b/packages/stream_chat_v1/lib/choose_user_page.dart index 7d3ce51..611804c 100644 --- a/packages/stream_chat_v1/lib/choose_user_page.dart +++ b/packages/stream_chat_v1/lib/choose_user_page.dart @@ -124,7 +124,7 @@ class ChooseUserPage extends StatelessWidget { arguments: HomePageArgs(client), ); }, - leading: UserAvatar( + leading: StreamUserAvatar( user: user, constraints: BoxConstraints.tight( Size.fromRadius(20), diff --git a/packages/stream_chat_v1/lib/group_chat_details_screen.dart b/packages/stream_chat_v1/lib/group_chat_details_screen.dart index 7dec664..6859f2b 100644 --- a/packages/stream_chat_v1/lib/group_chat_details_screen.dart +++ b/packages/stream_chat_v1/lib/group_chat_details_screen.dart @@ -1,7 +1,6 @@ import 'package:example/localizations.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:uuid/uuid.dart'; import 'channel_page.dart'; import 'routes/routes.dart'; @@ -155,7 +154,7 @@ class _GroupChatDetailsScreenState extends State { ), ], ), - body: ConnectionStatusBuilder( + body: StreamConnectionStatusBuilder( statusBuilder: (context, status) { String statusString = ''; bool showStatus = true; @@ -172,7 +171,7 @@ class _GroupChatDetailsScreenState extends State { statusString = AppLocalizations.of(context).disconnected; break; } - return InfoTile( + return StreamInfoTile( showMessage: showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -222,7 +221,7 @@ class _GroupChatDetailsScreenState extends State { final user = _selectedUsers[index]; return ListTile( key: ObjectKey(user), - leading: UserAvatar( + leading: StreamUserAvatar( user: user, constraints: BoxConstraints.tightFor( width: 40, diff --git a/packages/stream_chat_v1/lib/group_info_screen.dart b/packages/stream_chat_v1/lib/group_info_screen.dart index b518163..529c0a4 100644 --- a/packages/stream_chat_v1/lib/group_info_screen.dart +++ b/packages/stream_chat_v1/lib/group_info_screen.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:example/localizations.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_file_display_screen.dart'; @@ -12,10 +11,9 @@ import 'channel_media_display_screen.dart'; import 'channel_page.dart'; import 'chat_info_screen.dart'; import 'pinned_messages_screen.dart'; -import 'routes/routes.dart'; class GroupInfoScreen extends StatefulWidget { - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; const GroupInfoScreen({ Key? key, @@ -41,16 +39,52 @@ class _GroupInfoScreenState extends State { ValueNotifier mutedBool = ValueNotifier(false); + late final channel = StreamChannel.of(context).channel; + + late final userListController = StreamUserListController( + client: StreamChat.of(context).client, + limit: 25, + filter: Filter.and( + [ + if (_searchController!.text.isNotEmpty) + Filter.autoComplete('name', _userNameQuery), + Filter.notIn('id', [ + StreamChat.of(context).currentUser!.id, + ...channel.state!.members + .map(((e) => e.userId)) + .whereType(), + ]), + ], + ), + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + ); + void _userNameListener() { if (_searchController!.text == _userNameQuery) { return; } if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { - if (mounted && modalSetStateCallback != null) { - modalSetStateCallback!(() { - _userNameQuery = _searchController!.text; - }); + if (mounted) { + _userNameQuery = _searchController!.text; + userListController.filter = Filter.and( + [ + if (_searchController!.text.isNotEmpty) + Filter.autoComplete('name', _userNameQuery), + Filter.notIn('id', [ + StreamChat.of(context).currentUser!.id, + ...channel.state!.members + .map(((e) => e.userId)) + .whereType(), + ]), + ], + ); + userListController.doInitialLoad(); } }); } @@ -58,25 +92,28 @@ class _GroupInfoScreenState extends State { @override void initState() { super.initState(); - var channel = StreamChannel.of(context); + _nameController = TextEditingController.fromValue( - TextEditingValue( - text: (channel.channel.extraData['name'] as String?) ?? ''), + TextEditingValue(text: (channel.extraData['name'] as String?) ?? ''), ); _searchController = TextEditingController()..addListener(_userNameListener); _nameController!.addListener(() { setState(() {}); }); - mutedBool = ValueNotifier(StreamChannel.of(context).channel.isMuted); + mutedBool = ValueNotifier(channel.isMuted); + } + + @override + void dispose() { + userListController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - var channel = StreamChannel.of(context); - return StreamBuilder>( - stream: channel.channel.state!.membersStream, + stream: channel.state!.membersStream, builder: (context, snapshot) { if (!snapshot.hasData) { return Container( @@ -85,11 +122,6 @@ class _GroupInfoScreenState extends State { ); } - var userMember = snapshot.data!.firstWhereOrNull( - (e) => e.user!.id == StreamChat.of(context).currentUser!.id, - ); - var isOwner = userMember?.role == 'owner'; - return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: AppBar( @@ -100,7 +132,7 @@ class _GroupInfoScreenState extends State { title: Column( children: [ StreamBuilder( - stream: channel.channelStateStream, + stream: channel.state?.channelStateStream, builder: (context, state) { if (!state.hasData) { return Text( @@ -137,7 +169,7 @@ class _GroupInfoScreenState extends State { height: 3.0, ), Text( - '${channel.channel.memberCount} ${AppLocalizations.of(context).members}, ${snapshot.data?.where((e) => e.user!.online).length ?? 0} ${AppLocalizations.of(context).online}', + '${channel.memberCount} ${AppLocalizations.of(context).members}, ${snapshot.data?.where((e) => e.user!.online).length ?? 0} ${AppLocalizations.of(context).online}', style: TextStyle( color: StreamChatTheme.of(context) .colorTheme @@ -150,7 +182,8 @@ class _GroupInfoScreenState extends State { ), centerTitle: true, actions: [ - if (!channel.channel.isDistinct && isOwner) + if (channel.ownCapabilities + .contains(PermissionType.updateChannelMembers)) StreamNeumorphicButton( child: InkWell( onTap: () { @@ -174,7 +207,9 @@ class _GroupInfoScreenState extends State { height: 8.0, color: StreamChatTheme.of(context).colorTheme.disabled, ), - if (isOwner) _buildNameTile(), + if (channel.ownCapabilities + .contains(PermissionType.updateChannel)) + _buildNameTile(), _buildOptionListTiles(), ], ), @@ -224,7 +259,7 @@ class _GroupInfoScreenState extends State { horizontal: 8.0, vertical: 12.0, ), - child: UserAvatar( + child: StreamUserAvatar( user: member.user!, constraints: BoxConstraints.tightFor( height: 40.0, @@ -339,7 +374,6 @@ class _GroupInfoScreenState extends State { } Widget _buildNameTile() { - var channel = StreamChannel.of(context).channel; var channelName = (channel.extraData['name'] as String?) ?? ''; return Material( @@ -365,6 +399,8 @@ class _GroupInfoScreenState extends State { ), Expanded( child: TextField( + enabled: channel.ownCapabilities + .contains(PermissionType.updateChannel), focusNode: _focusNode, controller: _nameController, cursorColor: @@ -413,7 +449,7 @@ class _GroupInfoScreenState extends State { size: 24.0, ), onTap: () { - StreamChannel.of(context).channel.update({ + channel.update({ 'name': _nameController!.text.trim(), }).catchError((err) { setState(() { @@ -433,8 +469,6 @@ class _GroupInfoScreenState extends State { } Widget _buildOptionListTiles() { - var channel = StreamChannel.of(context); - return Column( children: [ // OptionListTile( @@ -449,48 +483,50 @@ class _GroupInfoScreenState extends State { // ), // onTap: () {}, // ), - StreamBuilder( - stream: StreamChannel.of(context).channel.isMutedStream, - builder: (context, snapshot) { - mutedBool.value = snapshot.data; + if (channel.ownCapabilities.contains(PermissionType.muteChannel)) + StreamBuilder( + stream: channel.isMutedStream, + builder: (context, snapshot) { + mutedBool.value = snapshot.data; - return OptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.appBg, - separatorColor: StreamChatTheme.of(context).colorTheme.disabled, - title: AppLocalizations.of(context).muteGroup, - titleTextStyle: StreamChatTheme.of(context).textTheme.body, - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: StreamSvgIcon.mute( - size: 24.0, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + return StreamOptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + separatorColor: + StreamChatTheme.of(context).colorTheme.disabled, + title: AppLocalizations.of(context).muteGroup, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.mute( + size: 24.0, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), ), - ), - trailing: snapshot.data == null - ? CircularProgressIndicator() - : ValueListenableBuilder( - valueListenable: mutedBool, - builder: (context, value, _) { - return CupertinoSwitch( - value: value!, - onChanged: (val) { - mutedBool.value = val; + trailing: snapshot.data == null + ? CircularProgressIndicator() + : ValueListenableBuilder( + valueListenable: mutedBool, + builder: (context, value, _) { + return CupertinoSwitch( + value: value!, + onChanged: (val) { + mutedBool.value = val; - if (snapshot.data!) { - channel.channel.unmute(); - } else { - channel.channel.mute(); - } - }, - ); - }), - onTap: () {}, - ); - }), - OptionListTile( + if (snapshot.data!) { + channel.unmute(); + } else { + channel.mute(); + } + }, + ); + }), + onTap: () {}, + ); + }), + StreamOptionListTile( title: AppLocalizations.of(context).pinnedMessages, tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, @@ -515,43 +551,13 @@ class _GroupInfoScreenState extends State { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: MessageSearchBloc( - child: PinnedMessagesScreen( - messageTheme: widget.messageTheme, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - onShowMessage: (m, c) async { - final client = StreamChat.of(context).client; - final message = m; - final channel = client.channel( - c.type, - id: c.id, - ); - if (channel.state == null) { - await channel.watch(); - } - Navigator.pushNamedAndRemoveUntil( - context, - Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.CHANNEL_LIST_PAGE), - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ), - ), + child: PinnedMessagesScreen(), ), ), ); }, ), - OptionListTile( + StreamOptionListTile( tileColor: StreamChatTheme.of(context).colorTheme.appBg, separatorColor: StreamChatTheme.of(context).colorTheme.disabled, title: AppLocalizations.of(context).photosAndVideos, @@ -577,43 +583,15 @@ class _GroupInfoScreenState extends State { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: MessageSearchBloc( - child: ChannelMediaDisplayScreen( - messageTheme: widget.messageTheme, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - paginationParams: PaginationParams(limit: 20), - onShowMessage: (m, c) async { - final client = StreamChat.of(context).client; - final message = m; - final channel = client.channel( - c.type, - id: c.id, - ); - if (channel.state == null) { - await channel.watch(); - } - await Navigator.pushNamed( - context, - Routes.CHANNEL_PAGE, - arguments: ChannelPageArgs( - channel: channel, - initialMessage: message, - ), - ); - }, - ), + child: ChannelMediaDisplayScreen( + messageTheme: widget.messageTheme, ), ), ), ); }, ), - OptionListTile( + StreamOptionListTile( tileColor: StreamChatTheme.of(context).colorTheme.appBg, separatorColor: StreamChatTheme.of(context).colorTheme.disabled, title: AppLocalizations.of(context).files, @@ -639,24 +617,17 @@ class _GroupInfoScreenState extends State { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: MessageSearchBloc( - child: ChannelFileDisplayScreen( - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - paginationParams: PaginationParams(limit: 20), - ), + child: ChannelFileDisplayScreen( + messageTheme: widget.messageTheme, ), ), ), ); }, ), - if (!channel.channel.isDistinct) - OptionListTile( + if (!channel.isDistinct && + channel.ownCapabilities.contains(PermissionType.leaveChannel)) + StreamOptionListTile( tileColor: StreamChatTheme.of(context).colorTheme.appBg, separatorColor: StreamChatTheme.of(context).colorTheme.disabled, title: AppLocalizations.of(context).leaveGroup, @@ -700,106 +671,81 @@ class _GroupInfoScreenState extends State { } void _buildAddUserModal(context) { - var channel = StreamChannel.of(context).channel; - showDialog( useRootNavigator: false, context: context, barrierColor: StreamChatTheme.of(context).colorTheme.overlay, builder: (context) { - return StatefulBuilder(builder: (context, modalSetState) { - modalSetStateCallback = modalSetState; - return Padding( - padding: EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0), - child: Material( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - ), - clipBehavior: Clip.antiAlias, - child: Scaffold( - body: UsersBloc( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: _buildTextInputSection(modalSetState), - ), - Expanded( - child: UserListView( - selectedUsers: {}, - onUserTap: (user, _) async { - _searchController!.clear(); + return Padding( + padding: EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0), + child: Material( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + clipBehavior: Clip.antiAlias, + child: Scaffold( + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: _buildTextInputSection(), + ), + Expanded( + child: StreamUserGridView( + controller: userListController, + onUserTap: (user) async { + _searchController!.clear(); - await channel.addMembers([user.id]); - Navigator.pop(context); - setState(() {}); - }, - crossAxisCount: 4, - limit: 25, - filter: Filter.and( - [ - if (_searchController!.text.isNotEmpty) - Filter.autoComplete('name', _userNameQuery), - Filter.notIn('id', [ - StreamChat.of(context).currentUser!.id, - ...channel.state!.members - .map(((e) => e.userId)) - .whereType(), - ]), - ], - ), - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - 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.search( - size: 96, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), - ), - Text(AppLocalizations.of(context) - .noUserMatchesTheseKeywords), - ], + await channel.addMembers([user.id]); + Navigator.pop(context); + setState(() {}); + }, + 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.search( + size: 96, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + ), ), - ), + Text(AppLocalizations.of(context) + .noUserMatchesTheseKeywords), + ], ), - ); - }, + ), + ), ); }, - ), - ), - ], + ); + }, + ), ), - ), + ], ), ), - ); - }); + ), + ); }, - ); + ).whenComplete(() { + _searchController?.clear(); + }); } - Widget _buildTextInputSection(modalSetState) { + Widget _buildTextInputSection() { final theme = StreamChatTheme.of(context); return Column( children: [ @@ -859,7 +805,6 @@ class _GroupInfoScreenState extends State { } void _showUserInfoModal(User? user, bool isUserAdmin) { - var channel = StreamChannel.of(context).channel; final color = StreamChatTheme.of(context).colorTheme.barsBg; showModalBottomSheet( @@ -896,7 +841,7 @@ class _GroupInfoScreenState extends State { Center( child: Padding( padding: const EdgeInsets.all(16.0), - child: UserAvatar( + child: StreamUserAvatar( user: user, constraints: BoxConstraints.tightFor( height: 64.0, diff --git a/packages/stream_chat_v1/lib/home_page.dart b/packages/stream_chat_v1/lib/home_page.dart index a53dd2c..e37b862 100644 --- a/packages/stream_chat_v1/lib/home_page.dart +++ b/packages/stream_chat_v1/lib/home_page.dart @@ -4,7 +4,6 @@ import 'package:example/channel_page.dart'; import 'package:example/notifications_service.dart'; import 'package:example/routes/app_routes.dart'; import 'package:example/routes/routes.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_v1/lib/new_chat_screen.dart b/packages/stream_chat_v1/lib/new_chat_screen.dart index eb247c3..f0ce098 100644 --- a/packages/stream_chat_v1/lib/new_chat_screen.dart +++ b/packages/stream_chat_v1/lib/new_chat_screen.dart @@ -19,6 +19,20 @@ class _NewChatScreenState extends State { late TextEditingController _controller; + late final userListController = StreamUserListController( + client: StreamChat.of(context).client, + limit: 25, + filter: Filter.and([ + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]), + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + ); + ChipInputTextFieldState? get _chipInputTextFieldState => _chipInputTextFieldStateKey.currentState; @@ -45,6 +59,12 @@ class _NewChatScreenState extends State { _userNameQuery = _controller.text; _isSearchActive = _userNameQuery.isNotEmpty; }); + userListController.filter = Filter.and([ + if (_userNameQuery.isNotEmpty) + Filter.autoComplete('name', _userNameQuery), + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]); + userListController.doInitialLoad(); }); } @@ -112,6 +132,7 @@ class _NewChatScreenState extends State { _controller.clear(); _controller.removeListener(_userNameListener); _controller.dispose(); + userListController.dispose(); super.dispose(); } @@ -130,7 +151,7 @@ class _NewChatScreenState extends State { ), centerTitle: true, ), - body: ConnectionStatusBuilder( + body: StreamConnectionStatusBuilder( statusBuilder: (context, status) { String statusString = ''; bool showStatus = true; @@ -147,7 +168,7 @@ class _NewChatScreenState extends State { statusString = AppLocalizations.of(context).disconnected; break; } - return InfoTile( + return StreamInfoTile( showMessage: showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -200,7 +221,7 @@ class _NewChatScreenState extends State { .overlay, shape: BoxShape.circle, ), - child: UserAvatar( + child: StreamUserAvatar( showOnlineStatus: false, user: user, constraints: BoxConstraints.tightFor( @@ -287,86 +308,81 @@ class _NewChatScreenState extends State { ? GestureDetector( behavior: HitTestBehavior.opaque, onPanDown: (_) => FocusScope.of(context).unfocus(), - child: UsersBloc( - child: UserListView( - selectedUsers: _selectedUsers, - groupAlphabetically: - _isSearchActive ? false : true, - onUserTap: (user, _) { - _controller.clear(); - if (!_selectedUsers.contains(user)) { - _chipInputTextFieldState - ?..addItem(user) - ..pauseItemAddition(); - } else { - _chipInputTextFieldState!.removeItem(user); - } - }, - limit: 25, - filter: Filter.and([ - if (_userNameQuery.isNotEmpty) - Filter.autoComplete('name', _userNameQuery), - Filter.notEqual('id', - StreamChat.of(context).currentUser!.id), - ]), - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - 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.search( - size: 96, - color: Colors.grey, - ), + child: StreamUserListView( + controller: userListController, + // groupAlphabetically: + // _isSearchActive ? false : true, + onUserTap: (user) { + _controller.clear(); + if (!_selectedUsers.contains(user)) { + _chipInputTextFieldState + ?..addItem(user) + ..pauseItemAddition(); + } else { + _chipInputTextFieldState!.removeItem(user); + } + }, + itemBuilder: ( + context, + users, + index, + defaultWidget, + ) { + return defaultWidget.copyWith( + selected: + _selectedUsers.contains(users[index]), + ); + }, + 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.search( + size: 96, + color: Colors.grey, ), - Text( - AppLocalizations.of(context) - .noUserMatchesTheseKeywords, - style: StreamChatTheme.of( - context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme - .of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5)), - ), - ], - ), + ), + Text( + AppLocalizations.of(context) + .noUserMatchesTheseKeywords, + style: StreamChatTheme.of( + context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme + .of(context) + .colorTheme + .textHighEmphasis + .withOpacity(.5)), + ), + ], ), ), - ); - }, - ); - }, - ), + ), + ); + }, + ); + }, ), ) : FutureBuilder( future: channel!.initialized, builder: (context, snapshot) { if (snapshot.data == true) { - return MessageListView(); + return StreamMessageListView(); } return Center( @@ -384,7 +400,7 @@ class _NewChatScreenState extends State { }, ), ), - MessageInput( + StreamMessageInput( focusNode: _messageInputFocusNode, preMessageSending: (message) async { await channel!.watch(); diff --git a/packages/stream_chat_v1/lib/new_group_chat_screen.dart b/packages/stream_chat_v1/lib/new_group_chat_screen.dart index 2d0842a..ff0c175 100644 --- a/packages/stream_chat_v1/lib/new_group_chat_screen.dart +++ b/packages/stream_chat_v1/lib/new_group_chat_screen.dart @@ -23,6 +23,20 @@ class _NewGroupChatScreenState extends State { Timer? _debounce; + late final userListController = StreamUserListController( + client: StreamChat.of(context).client, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + limit: 25, + filter: Filter.and([ + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]), + ); + void _userNameListener() { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { @@ -31,6 +45,12 @@ class _NewGroupChatScreenState extends State { _userNameQuery = _controller!.text; _isSearchActive = _userNameQuery.isNotEmpty; }); + userListController.filter = Filter.and([ + if (_userNameQuery.isNotEmpty) + Filter.autoComplete('name', _userNameQuery), + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]); + userListController.doInitialLoad(); } }); } @@ -46,6 +66,7 @@ class _NewGroupChatScreenState extends State { _controller?.clear(); _controller?.removeListener(_userNameListener); _controller?.dispose(); + userListController.dispose(); super.dispose(); } @@ -88,7 +109,7 @@ class _NewGroupChatScreenState extends State { ) ], ), - body: ConnectionStatusBuilder( + body: StreamConnectionStatusBuilder( statusBuilder: (context, status) { String statusString = ''; bool showStatus = true; @@ -105,7 +126,7 @@ class _NewGroupChatScreenState extends State { statusString = AppLocalizations.of(context).disconnected; break; } - return InfoTile( + return StreamInfoTile( showMessage: showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -136,7 +157,7 @@ class _NewGroupChatScreenState extends State { children: [ Stack( children: [ - UserAvatar( + StreamUserAvatar( onlineIndicatorAlignment: Alignment(0.9, 0.9), user: user, @@ -228,77 +249,66 @@ class _NewGroupChatScreenState extends State { body: GestureDetector( behavior: HitTestBehavior.opaque, onPanDown: (_) => FocusScope.of(context).unfocus(), - child: UsersBloc( - child: UserListView( - selectedUsers: _selectedUsers, - pullToRefresh: false, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - if (!_selectedUsers.contains(user)) { - setState(() { - _selectedUsers.add(user); - }); - } else { - setState(() { - _selectedUsers.remove(user); - }); - } - }, - limit: 25, - filter: Filter.and([ - if (_userNameQuery.isNotEmpty) - Filter.autoComplete('name', _userNameQuery), - Filter.notEqual( - 'id', StreamChat.of(context).currentUser!.id), - ]), - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - 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.search( - size: 96, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), + child: StreamUserListView( + controller: userListController, + // groupAlphabetically: _isSearchActive ? false : true, + itemBuilder: (context, items, index, defaultWidget) { + return defaultWidget.copyWith( + selected: _selectedUsers.contains(items[index]), + ); + }, + onUserTap: (user) { + if (!_selectedUsers.contains(user)) { + setState(() { + _selectedUsers.add(user); + }); + } else { + setState(() { + _selectedUsers.remove(user); + }); + } + }, + 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.search( + size: 96, + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, ), - Text( - AppLocalizations.of(context) - .noUserMatchesTheseKeywords, - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), - ), - ], - ), + ), + Text( + AppLocalizations.of(context) + .noUserMatchesTheseKeywords, + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + ), + ), + ], ), ), - ); - }, - ); - }, - ), + ), + ); + }, + ); + }, ), ), ), diff --git a/packages/stream_chat_v1/lib/pinned_messages_screen.dart b/packages/stream_chat_v1/lib/pinned_messages_screen.dart index a3470bd..7ab6d98 100644 --- a/packages/stream_chat_v1/lib/pinned_messages_screen.dart +++ b/packages/stream_chat_v1/lib/pinned_messages_screen.dart @@ -1,60 +1,34 @@ import 'package:example/localizations.dart'; +import 'package:example/routes/routes.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:video_player/video_player.dart'; + +import 'channel_page.dart'; class PinnedMessagesScreen extends StatefulWidget { - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. - /// Direction can be ascending or descending. - final List? sortOptions; - - /// Pagination parameters - /// limit: the number of users to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - final PaginationParams paginationParams; - - /// The builder used when the file list is empty. - final WidgetBuilder? emptyBuilder; - - final ShowMessageCallback? onShowMessage; - - final MessageThemeData messageTheme; - - const PinnedMessagesScreen({ - required this.messageTheme, - this.sortOptions, - this.paginationParams = const PaginationParams(limit: 20), - this.emptyBuilder, - this.onShowMessage, - }); - @override - _PinnedMessagesScreenState createState() => _PinnedMessagesScreenState(); + State createState() => _PinnedMessagesScreenState(); } class _PinnedMessagesScreenState extends State { - Map controllerCache = {}; - - @override - void initState() { - super.initState(); - final messageSearchBloc = MessageSearchBloc.of(context); - messageSearchBloc.search( - filter: Filter.in_( - 'cid', - [StreamChannel.of(context).channel.cid!], + late final controller = StreamMessageSearchListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.equal( + 'pinned', + true, + ), + sort: [ + SortOption( + 'created_at', + direction: SortOption.ASC, ), - messageFilter: Filter.equal( - 'pinned', - true, - ), - sort: widget.sortOptions, - pagination: widget.paginationParams, - ); - } + ], + limit: 20, + ); @override Widget build(BuildContext context) { @@ -73,25 +47,9 @@ class _PinnedMessagesScreenState extends State { leading: StreamBackButton(), backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, ), - body: _buildMediaGrid(), - ); - } - - Widget _buildMediaGrid() { - final messageSearchBloc = MessageSearchBloc.of(context); - - return StreamBuilder>( - builder: (context, snapshot) { - if (snapshot.data == null) { - return Center( - child: const CircularProgressIndicator(), - ); - } - - if (snapshot.data!.isEmpty) { - if (widget.emptyBuilder != null) { - return widget.emptyBuilder!(context); - } + body: StreamMessageSearchListView( + controller: controller, + emptyBuilder: (_) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -140,74 +98,33 @@ class _PinnedMessagesScreenState extends State { ], ), ); - } - - var data = snapshot.data ?? []; - - return LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( - filter: Filter.in_( - 'cid', - [StreamChannel.of(context).channel.cid!], + }, + onMessageTap: (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, ), - messageFilter: Filter.equal( - 'pinned', - true, - ), - sort: widget.sortOptions, - pagination: widget.paginationParams.copyWith( - offset: messageSearchBloc.messageResponses?.length ?? 0, - ), - ), - child: ListView.builder( - itemBuilder: (context, position) { - var user = data[position].message.user!; - var attachments = data[position].message.attachments; - var text = data[position].message.text ?? ''; - - return ListTile( - leading: UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - width: 40.0, - height: 40.0, - ), - borderRadius: BorderRadius.circular(28), - ), - title: Text( - user.name, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, - fontWeight: FontWeight.bold), - ), - subtitle: Text( - text != '' - ? text - : (attachments.isNotEmpty - ? '${attachments.length} ${attachments.length > 1 ? AppLocalizations.of(context).attachments : AppLocalizations.of(context).attachment}' - : ''), - ), - onTap: () { - widget.onShowMessage?.call(data[position].message, - StreamChannel.of(context).channel); - }, - ); - }, - itemCount: snapshot.data!.length, - ), - ); - }, - stream: messageSearchBloc.messagesStream, + ); + }, + ), ); } @override void dispose() { + controller.dispose(); super.dispose(); - for (var c in controllerCache.values) { - c!.dispose(); - } } } diff --git a/packages/stream_chat_v1/lib/routes/app_routes.dart b/packages/stream_chat_v1/lib/routes/app_routes.dart index 78593c4..4f913b4 100644 --- a/packages/stream_chat_v1/lib/routes/app_routes.dart +++ b/packages/stream_chat_v1/lib/routes/app_routes.dart @@ -1,4 +1,4 @@ -import 'package:collection/src/iterable_extensions.dart'; +import 'package:collection/collection.dart'; import 'package:example/channel_list_page.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_v1/lib/splash_screen.dart b/packages/stream_chat_v1/lib/splash_screen.dart index 2c89228..9e72572 100644 --- a/packages/stream_chat_v1/lib/splash_screen.dart +++ b/packages/stream_chat_v1/lib/splash_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:lottie/lottie.dart'; mixin SplashScreenStateMixin on State diff --git a/packages/stream_chat_v1/lib/thread_page.dart b/packages/stream_chat_v1/lib/thread_page.dart index ca8d67c..3983cb7 100644 --- a/packages/stream_chat_v1/lib/thread_page.dart +++ b/packages/stream_chat_v1/lib/thread_page.dart @@ -18,8 +18,15 @@ class ThreadPage extends StatefulWidget { } class _ThreadPageState extends State { - Message? _quotedMessage; FocusNode _focusNode = FocusNode(); + late StreamMessageInputController _messageInputController; + + @override + void initState() { + super.initState(); + _messageInputController = + StreamMessageInputController(message: widget.parent); + } @override void dispose() { @@ -28,7 +35,7 @@ class _ThreadPageState extends State { } void _reply(Message message) { - setState(() => _quotedMessage = message); + _messageInputController.quotedMessage = message; WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { _focusNode.requestFocus(); }); @@ -38,13 +45,13 @@ class _ThreadPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, - appBar: ThreadHeader( + appBar: StreamThreadHeader( parent: widget.parent, ), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( parentMessage: widget.parent, initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, @@ -56,22 +63,16 @@ class _ThreadPageState extends State { return defaultMessage.copyWith( onReplyTap: _reply, deletedBottomRowBuilder: (context, message) { - return const VisibleFootnote(); + return const StreamVisibleFootnote(); }, ); }, - pinPermissions: ['owner', 'admin', 'member'], ), ), if (widget.parent.type != 'deleted') - MessageInput( - parentMessage: widget.parent, + StreamMessageInput( focusNode: _focusNode, - quotedMessage: _quotedMessage, - onQuotedMessageCleared: () { - setState(() => _quotedMessage = null); - _focusNode.unfocus(); - }, + messageInputController: _messageInputController, ), ], ), diff --git a/packages/stream_chat_v1/lib/user_mentions_page.dart b/packages/stream_chat_v1/lib/user_mentions_page.dart index fa2d834..3dcbab6 100644 --- a/packages/stream_chat_v1/lib/user_mentions_page.dart +++ b/packages/stream_chat_v1/lib/user_mentions_page.dart @@ -5,85 +5,94 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_page.dart'; -class UserMentionsPage extends StatelessWidget { +class UserMentionsPage extends StatefulWidget { + @override + State createState() => _UserMentionsPageState(); +} + +class _UserMentionsPageState extends State { + late final controller = StreamMessageSearchListController( + client: StreamChat.of(context).client, + filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]), + messageFilter: Filter.custom( + operator: r'$contains', + key: 'mentioned_users.id', + value: StreamChat.of(context).currentUser!.id, + ), + sort: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + limit: 20, + ); @override Widget build(BuildContext context) { - final user = StreamChat.of(context).currentUser!; - return MessageSearchBloc( - child: MessageSearchListView( - filters: Filter.in_('members', [user.id]), - messageFilters: Filter.custom( - operator: r'$contains', - key: 'mentioned_users.id', - value: user.id, - ), - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - 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.disabled, - ), + return StreamMessageSearchListView( + controller: controller, + 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.disabled, ), - Text( - AppLocalizations.of(context).noMentionsExistYet, - style: StreamChatTheme.of(context) - .textTheme - .body - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), - ), - ], - ), + ), + Text( + AppLocalizations.of(context).noMentionsExistYet, + style: + StreamChatTheme.of(context).textTheme.body.copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textLowEmphasis, + ), + ), + ], ), ), - ); - }, - ); - }, - 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, - ), - ); - }, - ), + ), + ); + }, + ); + }, + onMessageTap: (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, + ), + ); + }, ); } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } } diff --git a/packages/stream_chat_v1/pubspec.yaml b/packages/stream_chat_v1/pubspec.yaml index 96af0ca..023a74f 100644 --- a/packages/stream_chat_v1/pubspec.yaml +++ b/packages/stream_chat_v1/pubspec.yaml @@ -13,17 +13,17 @@ dependencies: stream_chat_flutter: git: url: https://github.com/GetStream/stream-chat-flutter.git - ref: develop + ref: v4 path: packages/stream_chat_flutter stream_chat_persistence: git: url: https://github.com/GetStream/stream-chat-flutter.git - ref: develop + ref: v4 path: packages/stream_chat_persistence stream_chat_localizations: git: url: https://github.com/GetStream/stream-chat-flutter.git - ref: develop + ref: v4 path: packages/stream_chat_localizations flutter_local_notifications: ^9.0.0 flutter_svg: ^1.0.3 @@ -43,17 +43,17 @@ dependency_overrides: stream_chat: git: url: https://github.com/GetStream/stream-chat-flutter.git - ref: develop + ref: v4 path: packages/stream_chat stream_chat_flutter_core: git: url: https://github.com/GetStream/stream-chat-flutter.git - ref: develop + ref: v4 path: packages/stream_chat_flutter_core stream_chat_flutter: git: url: https://github.com/GetStream/stream-chat-flutter.git - ref: develop + ref: v4 path: packages/stream_chat_flutter flutter: