diff --git a/CHANGELOG.md b/CHANGELOG.md index 8585979..9b5988e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.4.0 +- Add way to create group chats +- Update flutter_profile to 1.3.0 +- Update flutter_image_picker to 1.0.5 + +## 1.3.1 + +- Added more options for styling the UI. +- Changed the way profile images are shown. +- Added an ontapUser in the chat. +- Changed the way the time is shown in the chat after a message. +- Added option to customize chat title and username chat message widget. + ## 1.2.1 - Fixed bug in the LocalChatService diff --git a/packages/flutter_chat/example/.gitignore b/packages/flutter_chat/example/.gitignore index e63bc79..dcf4fd3 100644 --- a/packages/flutter_chat/example/.gitignore +++ b/packages/flutter_chat/example/.gitignore @@ -32,6 +32,14 @@ ios .pub/ /build/ +# Platform-specific folders +**/android/ +**/ios/ +**/web/ +**/windows/ +**/macos/ +**/linux/ + # Symbolication related app.*.symbols diff --git a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart index 97f7b7a..21aaef9 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat/flutter_chat.dart'; +import 'package:uuid/uuid.dart'; /// Navigates to the chat user story screen. /// @@ -31,6 +32,7 @@ Widget _chatScreenRoute( BuildContext context, ) => ChatScreen( + unreadMessageTextStyle: configuration.unreadMessageTextStyle, service: configuration.chatService, options: configuration.chatOptionsBuilder(context), onNoChats: () async => Navigator.of(context).push( @@ -84,11 +86,31 @@ Widget _chatDetailScreenRoute( String chatId, ) => ChatDetailScreen( + chatTitleBuilder: configuration.chatTitleBuilder, + usernameBuilder: configuration.usernameBuilder, + loadingWidgetBuilder: configuration.loadingWidgetBuilder, + iconDisabledColor: configuration.iconDisabledColor, pageSize: configuration.messagePageSize, options: configuration.chatOptionsBuilder(context), translations: configuration.translations, service: configuration.chatService, chatId: chatId, + textfieldBottomPadding: configuration.textfieldBottomPadding ?? 0, + onPressUserProfile: (userId) async { + if (configuration.onPressUserProfile != null) { + return configuration.onPressUserProfile?.call(); + } + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatProfileScreenRoute( + configuration, + context, + chatId, + userId, + ), + ), + ); + }, onMessageSubmit: (message) async { if (configuration.onMessageSubmit != null) { await configuration.onMessageSubmit?.call(message); @@ -178,11 +200,25 @@ Widget _newChatScreenRoute( options: configuration.chatOptionsBuilder(context), translations: configuration.translations, service: configuration.chatService, + onPressCreateGroupChat: () async { + configuration.onPressCreateGroupChat?.call(); + if (context.mounted) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _newGroupChatScreenRoute( + configuration, + context, + ), + ), + ); + } + }, onPressCreateChat: (user) async { configuration.onPressCreateChat?.call(user); if (configuration.onPressCreateChat != null) return; var chat = await configuration.chatService.chatOverviewService .getChatByUser(user); + debugPrint('Chat is ${chat.id}'); if (chat.id == null) { chat = await configuration.chatService.chatOverviewService .storeChatIfNot( @@ -204,3 +240,64 @@ Widget _newChatScreenRoute( } }, ); + +Widget _newGroupChatScreenRoute( + ChatUserStoryConfiguration configuration, + BuildContext context, +) => + NewGroupChatScreen( + options: configuration.chatOptionsBuilder(context), + translations: configuration.translations, + service: configuration.chatService, + onPressGroupChatOverview: (users) async => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _newGroupChatOverviewScreenRoute( + configuration, + context, + users, + ), + ), + ), + ); + +Widget _newGroupChatOverviewScreenRoute( + ChatUserStoryConfiguration configuration, + BuildContext context, + List users, +) => + NewGroupChatOverviewScreen( + options: configuration.chatOptionsBuilder(context), + translations: configuration.translations, + service: configuration.chatService, + users: users, + onPressCompleteGroupChatCreation: (users, groupChatName) async { + configuration.onPressCompleteGroupChatCreation + ?.call(users, groupChatName); + if (configuration.onPressCreateGroupChat != null) return; + debugPrint('----------- The list of users = $users -----------'); + debugPrint('----------- Group chat name = $groupChatName -----------'); + + var chat = + await configuration.chatService.chatOverviewService.storeChatIfNot( + GroupChatModel( + id: const Uuid().v4(), + canBeDeleted: true, + title: groupChatName, + imageUrl: 'https://picsum.photos/200/300', + users: users, + ), + ); + debugPrint('----------- Chat id = ${chat.id} -----------'); + if (context.mounted) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatDetailScreenRoute( + configuration, + context, + chat.id!, + ), + ), + ); + } + }, + ); diff --git a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart b/packages/flutter_chat/lib/src/flutter_chat_userstory.dart index ffcf1f4..e5de04d 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_userstory.dart @@ -15,6 +15,7 @@ List getChatStoryRoutes( path: ChatUserStoryRoutes.chatScreen, pageBuilder: (context, state) { var chatScreen = ChatScreen( + unreadMessageTextStyle: configuration.unreadMessageTextStyle, service: configuration.chatService, options: configuration.chatOptionsBuilder(context), onNoChats: () async => @@ -53,11 +54,24 @@ List getChatStoryRoutes( pageBuilder: (context, state) { var chatId = state.pathParameters['id']; var chatDetailScreen = ChatDetailScreen( + chatTitleBuilder: configuration.chatTitleBuilder, + usernameBuilder: configuration.usernameBuilder, + loadingWidgetBuilder: configuration.loadingWidgetBuilder, + iconDisabledColor: configuration.iconDisabledColor, pageSize: configuration.messagePageSize, options: configuration.chatOptionsBuilder(context), translations: configuration.translations, service: configuration.chatService, chatId: chatId!, + textfieldBottomPadding: configuration.textfieldBottomPadding ?? 0, + onPressUserProfile: (userId) async { + if (configuration.onPressUserProfile != null) { + return configuration.onPressUserProfile?.call(); + } + return context.push( + ChatUserStoryRoutes.chatProfileScreenPath(chatId, userId), + ); + }, onMessageSubmit: (message) async { if (configuration.onMessageSubmit != null) { await configuration.onMessageSubmit?.call(message); @@ -129,6 +143,33 @@ List getChatStoryRoutes( ); } }, + onPressCreateGroupChat: () async => context.push( + ChatUserStoryRoutes.newGroupChatScreen, + ), + ); + return buildScreenWithoutTransition( + context: context, + state: state, + child: configuration.chatPageBuilder?.call( + context, + newChatScreen, + ) ?? + Scaffold( + body: newChatScreen, + ), + ); + }, + ), + GoRoute( + path: ChatUserStoryRoutes.newGroupChatScreen, + pageBuilder: (context, state) { + var newChatScreen = NewGroupChatScreen( + options: configuration.chatOptionsBuilder(context), + translations: configuration.translations, + service: configuration.chatService, + onPressGroupChatOverview: (user) async => context.push( + ChatUserStoryRoutes.newGroupChatOverviewScreen, + ), ); return buildScreenWithoutTransition( context: context, diff --git a/packages/flutter_chat/lib/src/models/chat_configuration.dart b/packages/flutter_chat/lib/src/models/chat_configuration.dart index f2449df..cd47146 100644 --- a/packages/flutter_chat/lib/src/models/chat_configuration.dart +++ b/packages/flutter_chat/lib/src/models/chat_configuration.dart @@ -21,7 +21,9 @@ class ChatUserStoryConfiguration { this.onReadChat, this.onUploadImage, this.onPressCreateChat, - this.iconColor, + this.onPressCreateGroupChat, + this.onPressCompleteGroupChatCreation, + this.iconColor = Colors.black, this.deleteChatDialog, this.disableDismissForPermanentChats = false, this.routeToNewChatIfEmpty = true, @@ -31,6 +33,12 @@ class ChatUserStoryConfiguration { this.afterMessageSent, this.messagePageSize = 20, this.onPressUserProfile, + this.textfieldBottomPadding = 20, + this.iconDisabledColor = Colors.grey, + this.unreadMessageTextStyle, + this.loadingWidgetBuilder, + this.usernameBuilder, + this.chatTitleBuilder, }); /// The service responsible for handling chat-related functionalities. @@ -65,6 +73,8 @@ class ChatUserStoryConfiguration { final Function(ChatUserModel)? onPressCreateChat; /// Builder for chat options based on context. + final Function(List, String)? onPressCompleteGroupChatCreation; + final Function()? onPressCreateGroupChat; final ChatOptions Function(BuildContext context) chatOptionsBuilder; /// If true, the user will be routed to the new chat screen if there are @@ -91,4 +101,10 @@ class ChatUserStoryConfiguration { /// Callback function triggered when user profile is pressed. final Function()? onPressUserProfile; + final double? textfieldBottomPadding; + final Color? iconDisabledColor; + final TextStyle? unreadMessageTextStyle; + final Widget? Function(BuildContext context)? loadingWidgetBuilder; + final Widget Function(String userFullName)? usernameBuilder; + final Widget Function(String chatTitle)? chatTitleBuilder; } diff --git a/packages/flutter_chat/lib/src/routes.dart b/packages/flutter_chat/lib/src/routes.dart index cad8ae1..fc399f7 100644 --- a/packages/flutter_chat/lib/src/routes.dart +++ b/packages/flutter_chat/lib/src/routes.dart @@ -13,6 +13,8 @@ mixin ChatUserStoryRoutes { static const String newChatScreen = '/new-chat'; /// Constructs the path for the chat profile screen. + static const String newGroupChatScreen = '/new-group-chat'; + static const String newGroupChatOverviewScreen = '/new-group-chat-overview'; static String chatProfileScreenPath(String chatId, String? userId) => '/chat-profile/$chatId/$userId'; diff --git a/packages/flutter_chat/pubspec.yaml b/packages/flutter_chat/pubspec.yaml index baef44a..b65c44f 100644 --- a/packages/flutter_chat/pubspec.yaml +++ b/packages/flutter_chat/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_chat description: A new Flutter package project. -version: 1.2.1 +version: 1.4.0 publish_to: none @@ -20,17 +20,18 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_chat path: packages/flutter_chat_view - ref: 1.2.1 + ref: 1.4.0 flutter_chat_interface: git: url: https://github.com/Iconica-Development/flutter_chat path: packages/flutter_chat_interface - ref: 1.2.1 + ref: 1.4.0 flutter_chat_local: git: url: https://github.com/Iconica-Development/flutter_chat path: packages/flutter_chat_local - ref: 1.2.1 + ref: 1.4.0 + uuid: ^4.3.3 dev_dependencies: flutter_iconica_analysis: diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart index 287d092..24915ae 100644 --- a/packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart +++ b/packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart @@ -238,6 +238,9 @@ class FirebaseChatDetailService onCancel: () async { await _subscription?.cancel(); _subscription = null; + _cumulativeMessages = []; + lastChat = chatId; + lastMessage = null; debugPrint('Canceling messages stream'); }, ); @@ -259,14 +262,16 @@ class FirebaseChatDetailService /// [pageSize]: The number of messages to fetch. /// [chatId]: The ID of the chat. @override - Future fetchMoreMessage(int pageSize, String chatId) async { - if (lastChat == null) { - lastChat = chatId; - } else if (lastChat != chatId) { + Future fetchMoreMessage( + int pageSize, + String chatId, + ) async { + if (lastChat != chatId) { _cumulativeMessages = []; lastChat = chatId; lastMessage = null; } + // get the x amount of last messages from the oldest message that is in // cumulative messages and add that to the list var messages = []; diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart index a5a15b4..5f0abca 100644 --- a/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart +++ b/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart @@ -90,11 +90,13 @@ class FirebaseChatOverviewService implements ChatOverviewService { for (var element in event.docChanges) { var chat = element.doc.data(); if (chat == null) return; + var otherUser = await _userService.getUser( chat.users.firstWhere( (element) => element != currentUser?.id, ), ); + var unread = await _addUnreadChatSubscription(chat.id!, currentUser!.id!); diff --git a/packages/flutter_chat_firebase/pubspec.yaml b/packages/flutter_chat_firebase/pubspec.yaml index a4b259d..be75c9f 100644 --- a/packages/flutter_chat_firebase/pubspec.yaml +++ b/packages/flutter_chat_firebase/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_chat_firebase description: A new Flutter package project. -version: 1.2.1 +version: 1.4.0 publish_to: none environment: @@ -23,7 +23,7 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_chat path: packages/flutter_chat_interface - ref: 1.2.1 + ref: 1.4.0 dev_dependencies: flutter_iconica_analysis: diff --git a/packages/flutter_chat_interface/lib/src/model/chat.dart b/packages/flutter_chat_interface/lib/src/model/chat.dart index 18636dc..e839cfb 100644 --- a/packages/flutter_chat_interface/lib/src/model/chat.dart +++ b/packages/flutter_chat_interface/lib/src/model/chat.dart @@ -6,6 +6,7 @@ import 'package:flutter_chat_interface/flutter_chat_interface.dart'; abstract class ChatModelInterface { + ChatModelInterface copyWith(); String? get id; List? get messages; int? get unreadMessages; @@ -55,4 +56,22 @@ class ChatModel implements ChatModelInterface { @override final bool canBeDeleted; + + @override + ChatModel copyWith({ + String? id, + List? messages, + int? unreadMessages, + DateTime? lastUsed, + ChatMessageModel? lastMessage, + bool? canBeDeleted, + }) => + ChatModel( + id: id ?? this.id, + messages: messages ?? this.messages, + unreadMessages: unreadMessages ?? this.unreadMessages, + lastUsed: lastUsed ?? this.lastUsed, + lastMessage: lastMessage ?? this.lastMessage, + canBeDeleted: canBeDeleted ?? this.canBeDeleted, + ); } diff --git a/packages/flutter_chat_interface/lib/src/model/group_chat.dart b/packages/flutter_chat_interface/lib/src/model/group_chat.dart index f94803a..d3f114a 100644 --- a/packages/flutter_chat_interface/lib/src/model/group_chat.dart +++ b/packages/flutter_chat_interface/lib/src/model/group_chat.dart @@ -19,6 +19,7 @@ abstract class GroupChatModelInterface extends ChatModel { String get imageUrl; List get users; + @override GroupChatModelInterface copyWith({ String? id, List? messages, diff --git a/packages/flutter_chat_interface/lib/src/model/personal_chat.dart b/packages/flutter_chat_interface/lib/src/model/personal_chat.dart index 7ae7732..91d6c00 100644 --- a/packages/flutter_chat_interface/lib/src/model/personal_chat.dart +++ b/packages/flutter_chat_interface/lib/src/model/personal_chat.dart @@ -17,6 +17,7 @@ abstract class PersonalChatModelInterface extends ChatModel { ChatUserModel get user; + @override PersonalChatModel copyWith({ String? id, List? messages, diff --git a/packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart b/packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart index 2fb39e3..6522b97 100644 --- a/packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart +++ b/packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart @@ -21,8 +21,10 @@ abstract class ChatDetailService with ChangeNotifier { String chatId, ); - /// Fetches more messages for the specified chat with a given page size. - Future fetchMoreMessage(int pageSize, String chatId); + Future fetchMoreMessage( + int pageSize, + String chatId, + ); /// Retrieves the list of messages for the chat. List getMessages(); diff --git a/packages/flutter_chat_interface/pubspec.yaml b/packages/flutter_chat_interface/pubspec.yaml index b317766..5e7f6c9 100644 --- a/packages/flutter_chat_interface/pubspec.yaml +++ b/packages/flutter_chat_interface/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_chat_interface description: A new Flutter package project. -version: 1.2.1 +version: 1.4.0 publish_to: none environment: diff --git a/packages/flutter_chat_local/lib/service/local_chat_detail_service.dart b/packages/flutter_chat_local/lib/service/local_chat_detail_service.dart index c9c5cb7..edf2006 100644 --- a/packages/flutter_chat_local/lib/service/local_chat_detail_service.dart +++ b/packages/flutter_chat_local/lib/service/local_chat_detail_service.dart @@ -25,7 +25,10 @@ class LocalChatDetailService with ChangeNotifier implements ChatDetailService { late StreamSubscription? _subscription; @override - Future fetchMoreMessage(int pageSize, String chatId) async { + Future fetchMoreMessage( + int pageSize, + String chatId, + ) async { await chatOverviewService.getChatById(chatId).then((value) { _cumulativeMessages.clear(); _cumulativeMessages.addAll(value.messages!); @@ -39,7 +42,9 @@ class LocalChatDetailService with ChangeNotifier implements ChatDetailService { List getMessages() => _cumulativeMessages; @override - Stream> getMessagesStream(String chatId) { + Stream> getMessagesStream( + String chatId, + ) { _controller.onListen = () async { _subscription = chatOverviewService.getChatById(chatId).asStream().listen((event) { @@ -73,7 +78,7 @@ class LocalChatDetailService with ChangeNotifier implements ChatDetailService { await (chatOverviewService as LocalChatOverviewService).updateChat( chat.copyWith( - messages: [...chat.messages!, message], + messages: [...?chat.messages, message], lastMessage: message, lastUsed: DateTime.now(), ), @@ -105,7 +110,7 @@ class LocalChatDetailService with ChangeNotifier implements ChatDetailService { ); await (chatOverviewService as LocalChatOverviewService).updateChat( chat.copyWith( - messages: [...chat.messages!, message], + messages: [...?chat.messages, message], lastMessage: message, lastUsed: DateTime.now(), ), diff --git a/packages/flutter_chat_local/lib/service/local_chat_overview_service.dart b/packages/flutter_chat_local/lib/service/local_chat_overview_service.dart index 88a3379..d8aa2f2 100644 --- a/packages/flutter_chat_local/lib/service/local_chat_overview_service.dart +++ b/packages/flutter_chat_local/lib/service/local_chat_overview_service.dart @@ -8,10 +8,10 @@ class LocalChatOverviewService with ChangeNotifier implements ChatOverviewService { /// The list of personal chat models. - final List _chats = []; + final List _chats = []; /// Retrieves the list of personal chat models. - List get chats => _chats; + List get chats => _chats; /// The stream controller for chats. final StreamController> _chatsController = @@ -19,9 +19,10 @@ class LocalChatOverviewService Future updateChat(ChatModel chat) { var index = _chats.indexWhere((element) => element.id == chat.id); - _chats[index] = chat as PersonalChatModel; + _chats[index] = chat; _chatsController.addStream(Stream.value(_chats)); notifyListeners(); + debugPrint('Chat updated: $chat'); return Future.value(); } @@ -30,24 +31,27 @@ class LocalChatOverviewService _chats.removeWhere((element) => element.id == chat.id); _chatsController.add(_chats); notifyListeners(); + debugPrint('Chat deleted: $chat'); return Future.value(); } @override - Future getChatById(String id) => - Future.value(_chats.firstWhere((element) => element.id == id)); + Future getChatById(String id) { + var chat = _chats.firstWhere((element) => element.id == id); + debugPrint('Retrieved chat by ID: $chat'); + debugPrint('Messages are: ${chat.messages?.length}'); + return Future.value(chat); + } @override - Future getChatByUser(ChatUserModel user) { + Future getChatByUser(ChatUserModel user) async { PersonalChatModel? chat; try { - chat = _chats.firstWhere( - (element) => element.user.id == user.id, - orElse: () { - throw Exception(); - }, - ); - } on Exception catch (_) { + chat = _chats + .whereType() + .firstWhere((element) => element.user.id == user.id); + // ignore: avoid_catching_errors + } on StateError { chat = PersonalChatModel( user: user, messages: [], @@ -55,11 +59,12 @@ class LocalChatOverviewService ); chat.id = chat.hashCode.toString(); _chats.add(chat); + debugPrint('New chat created: $chat'); } _chatsController.add([..._chats]); notifyListeners(); - return Future.value(chat); + return chat; } @override @@ -72,5 +77,18 @@ class LocalChatOverviewService Future readChat(ChatModel chat) async => Future.value(); @override - Future storeChatIfNot(ChatModel chat) => Future.value(chat); + Future storeChatIfNot(ChatModel chat) { + var chatExists = _chats.any((element) => element.id == chat.id); + + if (!chatExists) { + _chats.add(chat); + _chatsController.add([..._chats]); + notifyListeners(); + debugPrint('Chat stored: $chat'); + } else { + debugPrint('Chat already exists: $chat'); + } + + return Future.value(chat); + } } diff --git a/packages/flutter_chat_local/lib/service/local_chat_user_service.dart b/packages/flutter_chat_local/lib/service/local_chat_user_service.dart index 0836b48..5a0f5a7 100644 --- a/packages/flutter_chat_local/lib/service/local_chat_user_service.dart +++ b/packages/flutter_chat_local/lib/service/local_chat_user_service.dart @@ -16,10 +16,17 @@ class LocalChatUserService implements ChatUserService { lastName: 'Doe', imageUrl: 'https://picsum.photos/200/300', ), + ChatUserModel( + id: '3', + firstName: 'ico', + lastName: 'nica', + imageUrl: 'https://picsum.photos/100/200', + ), ]; @override - Future> getAllUsers() => Future.value(users); + Future> getAllUsers() => + Future.value(users.where((element) => element.id != '3').toList()); @override Future getCurrentUser() => Future.value(ChatUserModel()); diff --git a/packages/flutter_chat_local/pubspec.yaml b/packages/flutter_chat_local/pubspec.yaml index b33cb07..51986ed 100644 --- a/packages/flutter_chat_local/pubspec.yaml +++ b/packages/flutter_chat_local/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_chat_local description: "A new Flutter package project." -version: 1.2.1 +version: 1.4.0 publish_to: none homepage: @@ -15,7 +15,7 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_chat path: packages/flutter_chat_interface - ref: 1.2.1 + ref: 1.4.0 dev_dependencies: flutter_test: diff --git a/packages/flutter_chat_view/lib/flutter_chat_view.dart b/packages/flutter_chat_view/lib/flutter_chat_view.dart index 4a5e7c4..2ca8abe 100644 --- a/packages/flutter_chat_view/lib/flutter_chat_view.dart +++ b/packages/flutter_chat_view/lib/flutter_chat_view.dart @@ -13,3 +13,5 @@ export 'src/screens/chat_detail_screen.dart'; export 'src/screens/chat_profile_screen.dart'; export 'src/screens/chat_screen.dart'; export 'src/screens/new_chat_screen.dart'; +export 'src/screens/new_group_chat_overview_screen.dart'; +export 'src/screens/new_group_chat_screen.dart'; diff --git a/packages/flutter_chat_view/lib/src/components/chat_bottom.dart b/packages/flutter_chat_view/lib/src/components/chat_bottom.dart index 31e74d1..1571c98 100644 --- a/packages/flutter_chat_view/lib/src/components/chat_bottom.dart +++ b/packages/flutter_chat_view/lib/src/components/chat_bottom.dart @@ -13,6 +13,7 @@ class ChatBottom extends StatefulWidget { required this.translations, this.onPressSelectImage, this.iconColor, + this.iconDisabledColor, super.key, }); @@ -33,6 +34,7 @@ class ChatBottom extends StatefulWidget { /// The color of the icons. final Color? iconColor; + final Color? iconDisabledColor; @override State createState() => _ChatBottomState(); @@ -40,48 +42,61 @@ class ChatBottom extends StatefulWidget { class _ChatBottomState extends State { final TextEditingController _textEditingController = TextEditingController(); - + bool _isTyping = false; @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 17, - ), - child: SizedBox( - height: 45, - child: widget.messageInputBuilder( - _textEditingController, - Padding( - padding: const EdgeInsets.only(right: 15.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: widget.onPressSelectImage, - icon: Icon( - Icons.image, - color: widget.iconColor, - ), - ), - IconButton( - onPressed: () async { - var value = _textEditingController.text; - - if (value.isNotEmpty) { - await widget.onMessageSubmit(value); - _textEditingController.clear(); - } - }, - icon: Icon( - Icons.send, - color: widget.iconColor, - ), - ), - ], + Widget build(BuildContext context) { + _textEditingController.addListener(() { + if (_textEditingController.text.isEmpty) { + setState(() { + _isTyping = false; + }); + } else { + setState(() { + _isTyping = true; + }); + } + }); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, + ), + child: SizedBox( + height: 45, + child: widget.messageInputBuilder( + _textEditingController, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: widget.onPressSelectImage, + icon: Icon( + Icons.image, + color: widget.iconColor, + ), ), - ), - widget.translations, + IconButton( + disabledColor: widget.iconDisabledColor, + color: widget.iconColor, + onPressed: _isTyping + ? () async { + var value = _textEditingController.text; + + if (value.isNotEmpty) { + await widget.onMessageSubmit(value); + _textEditingController.clear(); + } + } + : null, + icon: const Icon( + Icons.send, + ), + ), + ], ), + widget.translations, ), - ); + ), + ); + } } diff --git a/packages/flutter_chat_view/lib/src/components/chat_detail_row.dart b/packages/flutter_chat_view/lib/src/components/chat_detail_row.dart index 5838096..3140518 100644 --- a/packages/flutter_chat_view/lib/src/components/chat_detail_row.dart +++ b/packages/flutter_chat_view/lib/src/components/chat_detail_row.dart @@ -13,6 +13,8 @@ class ChatDetailRow extends StatefulWidget { required this.translations, required this.message, required this.userAvatarBuilder, + required this.onPressUserProfile, + this.usernameBuilder, this.previousMessage, this.showTime = false, super.key, @@ -29,6 +31,8 @@ class ChatDetailRow extends StatefulWidget { /// The previous chat message model. final ChatMessageModel? previousMessage; + final Function(String? userId) onPressUserProfile; + final Widget Function(String userFullName)? usernameBuilder; /// Flag indicating whether to show the time. final bool showTime; @@ -46,6 +50,10 @@ class _ChatDetailRowState extends State { widget.message.timestamp.day != widget.previousMessage?.timestamp.day; var isSameSender = widget.previousMessage == null || widget.previousMessage?.sender.id != widget.message.sender.id; + var isSameMinute = widget.previousMessage != null && + widget.message.timestamp.minute == + widget.previousMessage?.timestamp.minute; + var hasHeader = isNewDate || isSameSender; return Padding( padding: EdgeInsets.only( @@ -55,17 +63,22 @@ class _ChatDetailRowState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (isNewDate || isSameSender) ...[ - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: widget.message.sender.imageUrl != null && - widget.message.sender.imageUrl!.isNotEmpty - ? ChatImage( - image: widget.message.sender.imageUrl!, - ) - : widget.userAvatarBuilder( - widget.message.sender, - 30, - ), + GestureDetector( + onTap: () => widget.onPressUserProfile( + widget.message.sender.id, + ), + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: widget.message.sender.imageUrl != null && + widget.message.sender.imageUrl!.isNotEmpty + ? ChatImage( + image: widget.message.sender.imageUrl!, + ) + : widget.userAvatarBuilder( + widget.message.sender, + 40, + ), + ), ), ] else ...[ const SizedBox( @@ -83,16 +96,23 @@ class _ChatDetailRowState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - widget.message.sender.fullName?.toUpperCase() ?? - widget.translations.anonymousUser, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: - Theme.of(context).textTheme.labelMedium?.color, + if (widget.usernameBuilder != null) + widget.usernameBuilder!( + widget.message.sender.fullName ?? '', + ) + else + Text( + widget.message.sender.fullName?.toUpperCase() ?? + widget.translations.anonymousUser, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color, + ), ), - ), Padding( padding: const EdgeInsets.only(top: 5.0), child: Text( @@ -111,35 +131,41 @@ class _ChatDetailRowState extends State { Padding( padding: const EdgeInsets.only(top: 3.0), child: widget.message is ChatTextMessageModel - ? RichText( - text: TextSpan( - text: + ? Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( (widget.message as ChatTextMessageModel).text, - style: TextStyle( - fontSize: 16, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color, + style: TextStyle( + fontSize: 16, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color, + ), + ), ), - children: [ - if (widget.showTime) - TextSpan( - text: " ${_dateFormatter.format( - date: widget.message.timestamp, - showFullDate: true, - ).split(' ').last}", - style: const TextStyle( - fontSize: 12, - color: Color(0xFFBBBBBB), - ), - ) - else - const TextSpan(), - ], - ), - overflow: TextOverflow.ellipsis, - maxLines: 999, + if (widget.showTime && + !isSameMinute && + !isNewDate && + !hasHeader) + Text( + _dateFormatter + .format( + date: widget.message.timestamp, + showFullDate: true, + ) + .split(' ') + .last, + style: const TextStyle( + fontSize: 12, + color: Color(0xFFBBBBBB), + ), + textAlign: TextAlign.end, + ), + ], ) : CachedNetworkImage( imageUrl: (widget.message as ChatImageMessageModel) diff --git a/packages/flutter_chat_view/lib/src/components/chat_image.dart b/packages/flutter_chat_view/lib/src/components/chat_image.dart index d89eee4..b4f5915 100644 --- a/packages/flutter_chat_view/lib/src/components/chat_image.dart +++ b/packages/flutter_chat_view/lib/src/components/chat_image.dart @@ -35,6 +35,7 @@ class ChatImage extends StatelessWidget { height: size, child: image.isNotEmpty ? CachedNetworkImage( + fadeInDuration: Duration.zero, imageUrl: image, fit: BoxFit.cover, ) diff --git a/packages/flutter_chat_view/lib/src/config/chat_options.dart b/packages/flutter_chat_view/lib/src/config/chat_options.dart index d8c9e43..b8ff8d1 100644 --- a/packages/flutter_chat_view/lib/src/config/chat_options.dart +++ b/packages/flutter_chat_view/lib/src/config/chat_options.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_view/flutter_chat_view.dart'; import 'package:flutter_chat_view/src/components/chat_image.dart'; import 'package:flutter_image_picker/flutter_image_picker.dart'; +import 'package:flutter_profile/flutter_profile.dart'; class ChatOptions { const ChatOptions({ @@ -50,13 +51,17 @@ Widget _createNewChatButton( ChatTranslations translations, ) => Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(24.0), child: ElevatedButton( style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, minimumSize: const Size.fromHeight(50), ), onPressed: onPressed, - child: Text(translations.newChatButton), + child: Text( + translations.newChatButton, + style: const TextStyle(color: Colors.white), + ), ), ); @@ -66,9 +71,38 @@ Widget _createMessageInput( ChatTranslations translations, ) => TextField( + textCapitalization: TextCapitalization.sentences, controller: textEditingController, decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(26.5), + borderSide: const BorderSide( + color: Colors.black, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(26.5), + borderSide: const BorderSide( + color: Colors.black, + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 30, + ), hintText: translations.messagePlaceholder, + hintStyle: const TextStyle( + fontWeight: FontWeight.normal, + color: Colors.black, + ), + fillColor: Colors.white, + filled: true, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(26.5), + ), + borderSide: BorderSide.none, + ), suffixIcon: suffixIcon, ), ); @@ -93,9 +127,13 @@ Widget _createImagePickerContainer( color: Colors.white, child: ImagePicker( customButton: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + ), onPressed: onClose, child: Text( translations.cancelImagePickerBtn, + style: const TextStyle(color: Colors.white), ), ), ), @@ -114,8 +152,12 @@ Widget _createUserAvatar( ChatUserModel user, double size, ) => - ChatImage( - image: user.imageUrl ?? '', + Avatar( + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl, + ), size: size, ); Widget _createGroupAvatar( diff --git a/packages/flutter_chat_view/lib/src/config/chat_translations.dart b/packages/flutter_chat_view/lib/src/config/chat_translations.dart index f4a2d56..3343857 100644 --- a/packages/flutter_chat_view/lib/src/config/chat_translations.dart +++ b/packages/flutter_chat_view/lib/src/config/chat_translations.dart @@ -7,6 +7,7 @@ class ChatTranslations { this.chatsTitle = 'Chats', this.chatsUnread = 'unread', this.newChatButton = 'Start chat', + this.newGroupChatButton = 'Start group chat', this.newChatTitle = 'Start chat', this.image = 'Image', this.searchPlaceholder = 'Search...', @@ -28,6 +29,7 @@ class ChatTranslations { final String chatsTitle; final String chatsUnread; final String newChatButton; + final String newGroupChatButton; final String newChatTitle; final String deleteChatButton; final String image; diff --git a/packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart b/packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart index dcbaa6a..5987a06 100644 --- a/packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart +++ b/packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_chat_view/flutter_chat_view.dart'; import 'package:flutter_chat_view/src/components/chat_bottom.dart'; import 'package:flutter_chat_view/src/components/chat_detail_row.dart'; @@ -21,9 +20,15 @@ class ChatDetailScreen extends StatefulWidget { required this.service, required this.pageSize, required this.chatId, + required this.textfieldBottomPadding, + required this.onPressChatTitle, + required this.onPressUserProfile, + this.chatTitleBuilder, + this.usernameBuilder, + this.loadingWidgetBuilder, this.translations = const ChatTranslations(), - this.onPressChatTitle, this.iconColor, + this.iconDisabledColor, this.showTime = false, super.key, }); @@ -39,13 +44,20 @@ class ChatDetailScreen extends StatefulWidget { // called at the start of the screen to set the chat to read // or when a new message is received final Future Function(ChatModel chat) onReadChat; - final Function(BuildContext context, ChatModel chat)? onPressChatTitle; + final Function(BuildContext context, ChatModel chat) onPressChatTitle; /// The color of the icon buttons in the chat bottom. final Color? iconColor; final bool showTime; final ChatService service; final int pageSize; + final double textfieldBottomPadding; + final Color? iconDisabledColor; + final Function(String? userId) onPressUserProfile; + // ignore: avoid_positional_boolean_parameters + final Widget? Function(BuildContext context)? loadingWidgetBuilder; + final Widget Function(String userFullName)? usernameBuilder; + final Widget Function(String chatTitle)? chatTitleBuilder; @override State createState() => _ChatDetailScreenState(); @@ -71,7 +83,7 @@ class _ChatDetailScreenState extends State { chat = await widget.service.chatOverviewService.getChatById(widget.chatId); - if (detailRows.isEmpty) { + if (detailRows.isEmpty && context.mounted) { await widget.service.chatDetailService.fetchMoreMessage( widget.pageSize, chat!.id!, @@ -99,6 +111,8 @@ class _ChatDetailScreenState extends State { translations: widget.translations, userAvatarBuilder: widget.options.userAvatarBuilder, previousMessage: previousMessage, + onPressUserProfile: widget.onPressUserProfile, + usernameBuilder: widget.usernameBuilder, ), ); previousMessage = message; @@ -123,6 +137,8 @@ class _ChatDetailScreenState extends State { @override Widget build(BuildContext context) { + var theme = Theme.of(context); + Future onPressSelectImage() async => showModalBottomSheet( context: context, builder: (BuildContext context) => @@ -132,14 +148,13 @@ class _ChatDetailScreenState extends State { ), ).then( (image) async { + if (image == null) return; var messenger = ScaffoldMessenger.of(context) ..showSnackBar( getImageLoadingSnackbar(widget.translations), ) ..activate(); - if (image != null) { - await widget.onUploadImage(image); - } + await widget.onUploadImage(image); Future.delayed(const Duration(seconds: 1), () { messenger.hideCurrentSnackBar(); }); @@ -153,9 +168,22 @@ class _ChatDetailScreenState extends State { var chatModel = snapshot.data; return Scaffold( appBar: AppBar( + backgroundColor: theme.appBarTheme.backgroundColor ?? Colors.black, + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), centerTitle: true, + leading: (chatModel is GroupChatModel) + ? GestureDetector( + onTap: () { + Navigator.popUntil(context, (route) => route.isFirst); + }, + child: const Icon( + Icons.arrow_back, + ), + ) + : null, title: GestureDetector( - onTap: () => widget.onPressChatTitle?.call(context, chatModel!), + onTap: () => widget.onPressChatTitle.call(context, chatModel!), child: Row( mainAxisSize: MainAxisSize.min, children: chat == null @@ -177,77 +205,103 @@ class _ChatDetailScreenState extends State { Expanded( child: Padding( padding: const EdgeInsets.only(left: 15.5), - child: Text( - (chatModel is GroupChatModel) - ? chatModel.title - : (chatModel is PersonalChatModel) - ? chatModel.user.fullName ?? - widget.translations.anonymousUser - : '', - style: const TextStyle(fontSize: 18), - ), + child: widget.chatTitleBuilder != null + ? widget.chatTitleBuilder!.call( + (chatModel is GroupChatModel) + ? chatModel.title + : (chatModel is PersonalChatModel) + ? chatModel.user.fullName ?? + widget + .translations.anonymousUser + : '', + ) + : Text( + (chatModel is GroupChatModel) + ? chatModel.title + : (chatModel is PersonalChatModel) + ? chatModel.user.fullName ?? + widget + .translations.anonymousUser + : '', + style: theme.appBarTheme.titleTextStyle ?? + const TextStyle( + color: Colors.white, + ), + ), ), ), ], ), ), ), - body: Column( + body: Stack( children: [ - Expanded( - child: Listener( - onPointerMove: (event) async { - var isTop = controller.position.pixels == - controller.position.maxScrollExtent; - - if (!showIndicator && - !isTop && - controller.position.userScrollDirection == - ScrollDirection.reverse) { - setState(() { - showIndicator = true; - }); - await widget.service.chatDetailService - .fetchMoreMessage(widget.pageSize, widget.chatId); - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { + Column( + children: [ + Expanded( + child: Listener( + onPointerMove: (event) async { + if (!showIndicator && + controller.offset >= + controller.position.maxScrollExtent && + !controller.position.outOfRange) { setState(() { - showIndicator = false; + showIndicator = true; + }); + await widget.service.chatDetailService + .fetchMoreMessage( + widget.pageSize, + widget.chatId, + ); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + showIndicator = false; + }); + } }); } - }); - } - }, - child: ListView( - shrinkWrap: true, - physics: const AlwaysScrollableScrollPhysics(), - controller: controller, - reverse: true, - padding: const EdgeInsets.only(top: 24.0), - children: [ - ...detailRows, - if (showIndicator) ...[ - const SizedBox( + }, + child: ListView( + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + controller: controller, + reverse: true, + padding: const EdgeInsets.only(top: 24.0), + children: [ + ...detailRows, + ], + ), + ), + ), + if (chatModel != null) + ChatBottom( + chat: chatModel, + messageInputBuilder: widget.options.messageInputBuilder, + onPressSelectImage: onPressSelectImage, + onMessageSubmit: widget.onMessageSubmit, + translations: widget.translations, + iconColor: widget.iconColor, + iconDisabledColor: widget.iconDisabledColor, + ), + SizedBox( + height: widget.textfieldBottomPadding, + ), + ], + ), + if (showIndicator) + widget.loadingWidgetBuilder?.call(context) ?? + const Column( + children: [ + SizedBox( height: 10, ), - const Center(child: CircularProgressIndicator()), - const SizedBox( + Center(child: CircularProgressIndicator()), + SizedBox( height: 10, ), ], - ], - ), - ), - ), - if (chatModel != null) - ChatBottom( - chat: chatModel, - messageInputBuilder: widget.options.messageInputBuilder, - onPressSelectImage: onPressSelectImage, - onMessageSubmit: widget.onMessageSubmit, - translations: widget.translations, - iconColor: widget.iconColor, - ), + ), ], ), ); diff --git a/packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart b/packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart index 2c71f5a..5e92b55 100644 --- a/packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart +++ b/packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart @@ -37,6 +37,7 @@ class _ProfileScreenState extends State { Widget build(BuildContext context) { var size = MediaQuery.of(context).size; var hasUser = widget.userId == null; + var theme = Theme.of(context); return FutureBuilder( future: hasUser // ignore: discarded_futures @@ -69,6 +70,9 @@ class _ProfileScreenState extends State { return Scaffold( appBar: AppBar( + backgroundColor: theme.appBarTheme.backgroundColor ?? Colors.black, + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), title: Text( (data is ChatUserModel) ? '${data.firstName ?? ''} ${data.lastName ?? ''}' @@ -77,6 +81,10 @@ class _ProfileScreenState extends State { : (data is GroupChatModel) ? data.title : '', + style: theme.appBarTheme.titleTextStyle ?? + const TextStyle( + color: Colors.white, + ), ), ), body: snapshot.hasData diff --git a/packages/flutter_chat_view/lib/src/screens/chat_screen.dart b/packages/flutter_chat_view/lib/src/screens/chat_screen.dart index f96f44d..78aa547 100644 --- a/packages/flutter_chat_view/lib/src/screens/chat_screen.dart +++ b/packages/flutter_chat_view/lib/src/screens/chat_screen.dart @@ -17,6 +17,7 @@ class ChatScreen extends StatefulWidget { required this.onPressChat, required this.onDeleteChat, required this.service, + this.unreadMessageTextStyle, this.onNoChats, this.deleteChatDialog, this.translations = const ChatTranslations(), @@ -50,6 +51,7 @@ class ChatScreen extends StatefulWidget { /// Disables the swipe to dismiss feature for chats that are not deletable. final bool disableDismissForPermanentChats; + final TextStyle? unreadMessageTextStyle; @override State createState() => _ChatScreenState(); @@ -72,9 +74,17 @@ class _ChatScreenState extends State { @override Widget build(BuildContext context) { var translations = widget.translations; + var theme = Theme.of(context); return widget.options.scaffoldBuilder( AppBar( - title: Text(translations.chatsTitle), + backgroundColor: theme.appBarTheme.backgroundColor ?? Colors.black, + title: Text( + translations.chatsTitle, + style: theme.appBarTheme.titleTextStyle ?? + const TextStyle( + color: Colors.white, + ), + ), centerTitle: true, actions: [ StreamBuilder( @@ -86,10 +96,11 @@ class _ChatScreenState extends State { padding: const EdgeInsets.only(right: 22.0), child: Text( '${snapshot.data ?? 0} ${translations.chatsUnread}', - style: const TextStyle( - color: Color(0xFFBBBBBB), - fontSize: 14, - ), + style: widget.unreadMessageTextStyle ?? + const TextStyle( + color: Colors.white, + fontSize: 14, + ), ), ), ), diff --git a/packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart b/packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart index 07b6d79..fd5af63 100644 --- a/packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart +++ b/packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart @@ -10,6 +10,7 @@ class NewChatScreen extends StatefulWidget { required this.options, required this.onPressCreateChat, required this.service, + required this.onPressCreateGroupChat, this.translations = const ChatTranslations(), super.key, }); @@ -22,6 +23,7 @@ class NewChatScreen extends StatefulWidget { /// Callback function for creating a new chat with a user. final Function(ChatUserModel) onPressCreateChat; + final Function() onPressCreateGroupChat; /// Translations for the chat. final ChatTranslations translations; @@ -36,62 +38,148 @@ class _NewChatScreenState extends State { String query = ''; @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: _buildSearchField(), - actions: [ - _buildSearchIcon(), - ], - ), - body: FutureBuilder>( - // ignore: discarded_futures - future: widget.service.chatUserService.getAllUsers(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - return _buildUserList(snapshot.data!); - } else { - return widget.options - .noChatsPlaceholderBuilder(widget.translations); - } - }, - ), - ); - - Widget _buildSearchField() => _isSearching - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: TextField( - focusNode: _textFieldFocusNode, - onChanged: (value) { - setState(() { - query = value; - }); + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + backgroundColor: theme.appBarTheme.backgroundColor ?? Colors.black, + title: _buildSearchField(), + actions: [ + _buildSearchIcon(), + ], + ), + body: Column( + children: [ + GestureDetector( + onTap: () async { + await widget.onPressCreateGroupChat(); }, - decoration: InputDecoration( - hintText: widget.translations.searchPlaceholder, + child: Container( + color: Colors.grey[900], + child: SizedBox( + height: 60.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 16.0, + ), + child: IconButton( + icon: const Icon( + Icons.group, + color: Colors.white, + ), + onPressed: () { + // Handle group chat creation + }, + ), + ), + const Text( + 'Create group chat', + style: TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + ], + ), + ], + ), + ), ), ), - ) - : Text(widget.translations.newChatButton); + Expanded( + child: FutureBuilder>( + // ignore: discarded_futures + future: widget.service.chatUserService.getAllUsers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + return _buildUserList(snapshot.data!); + } else { + return widget.options + .noChatsPlaceholderBuilder(widget.translations); + } + }, + ), + ), + ], + ), + ); + } - Widget _buildSearchIcon() => IconButton( - onPressed: () { - setState(() { - _isSearching = !_isSearching; - }); + Widget _buildSearchField() { + var theme = Theme.of(context); - if (_isSearching) { - _textFieldFocusNode.requestFocus(); - } - }, - icon: Icon( - _isSearching ? Icons.close : Icons.search, - ), - ); + return _isSearching + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextField( + focusNode: _textFieldFocusNode, + onChanged: (value) { + setState(() { + query = value; + }); + }, + decoration: InputDecoration( + hintText: widget.translations.searchPlaceholder, + hintStyle: theme.inputDecorationTheme.hintStyle ?? + const TextStyle( + color: Colors.white, + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: theme.inputDecorationTheme.focusedBorder?.borderSide + .color ?? + Colors.white, + ), + ), + ), + style: theme.inputDecorationTheme.hintStyle ?? + const TextStyle( + color: Colors.white, + ), + cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, + ), + ) + : Text( + widget.translations.newChatButton, + style: theme.appBarTheme.titleTextStyle ?? + const TextStyle( + color: Colors.white, + ), + ); + } + + Widget _buildSearchIcon() { + var theme = Theme.of(context); + + return IconButton( + onPressed: () { + setState(() { + _isSearching = !_isSearching; + query = ''; + }); + + if (_isSearching) { + _textFieldFocusNode.requestFocus(); + } + }, + icon: Icon( + _isSearching ? Icons.close : Icons.search, + color: theme.appBarTheme.iconTheme?.color ?? Colors.white, + ), + ); + } Widget _buildUserList(List users) { var filteredUsers = users @@ -114,12 +202,15 @@ class _NewChatScreenState extends State { var user = filteredUsers[index]; return GestureDetector( child: widget.options.chatRowContainerBuilder( - ChatRow( - avatar: widget.options.userAvatarBuilder( - user, - 40.0, + Container( + color: Colors.transparent, + child: ChatRow( + avatar: widget.options.userAvatarBuilder( + user, + 40.0, + ), + title: user.fullName ?? widget.translations.anonymousUser, ), - title: user.fullName ?? widget.translations.anonymousUser, ), ), onTap: () async { diff --git a/packages/flutter_chat_view/lib/src/screens/new_group_chat_overview_screen.dart b/packages/flutter_chat_view/lib/src/screens/new_group_chat_overview_screen.dart new file mode 100644 index 0000000..64bf286 --- /dev/null +++ b/packages/flutter_chat_view/lib/src/screens/new_group_chat_overview_screen.dart @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_chat_view/flutter_chat_view.dart'; + +class NewGroupChatOverviewScreen extends StatefulWidget { + const NewGroupChatOverviewScreen({ + required this.options, + required this.onPressCompleteGroupChatCreation, + required this.service, + required this.users, + this.translations = const ChatTranslations(), + super.key, + }); + + final ChatOptions options; + final ChatTranslations translations; + final ChatService service; + final List users; + final Function(List, String) onPressCompleteGroupChatCreation; + + @override + State createState() => + _NewGroupChatOverviewScreenState(); +} + +class _NewGroupChatOverviewScreenState + extends State { + final TextEditingController _textEditingController = TextEditingController(); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + backgroundColor: theme.appBarTheme.backgroundColor ?? Colors.black, + title: const Text( + 'New Group Chat', + style: TextStyle(color: Colors.white), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _textEditingController, + decoration: const InputDecoration( + hintText: 'Group chat name', + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + await widget.onPressCompleteGroupChatCreation( + widget.users, + _textEditingController.text, + ); + }, + child: const Icon(Icons.check_circle), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + ); + } +} diff --git a/packages/flutter_chat_view/lib/src/screens/new_group_chat_screen.dart b/packages/flutter_chat_view/lib/src/screens/new_group_chat_screen.dart new file mode 100644 index 0000000..a4a5251 --- /dev/null +++ b/packages/flutter_chat_view/lib/src/screens/new_group_chat_screen.dart @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_chat_view/flutter_chat_view.dart'; + +class NewGroupChatScreen extends StatefulWidget { + const NewGroupChatScreen({ + required this.options, + required this.onPressGroupChatOverview, + required this.service, + this.translations = const ChatTranslations(), + super.key, + }); + + final ChatOptions options; + final ChatTranslations translations; + final ChatService service; + final Function(List) onPressGroupChatOverview; + + @override + State createState() => _NewGroupChatScreenState(); +} + +class _NewGroupChatScreenState extends State { + final FocusNode _textFieldFocusNode = FocusNode(); + List selectedUserList = []; + + bool _isSearching = false; + String query = ''; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + backgroundColor: theme.appBarTheme.backgroundColor ?? Colors.black, + title: _buildSearchField(), + actions: [ + _buildSearchIcon(), + ], + ), + body: FutureBuilder>( + // ignore: discarded_futures + future: widget.service.chatUserService.getAllUsers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + return _buildUserList(snapshot.data!); + } else { + return widget.options + .noChatsPlaceholderBuilder(widget.translations); + } + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + await widget.onPressGroupChatOverview(selectedUserList); + }, + child: const Icon(Icons.arrow_circle_right), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + ); + } + + Widget _buildSearchField() { + var theme = Theme.of(context); + + return _isSearching + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextField( + focusNode: _textFieldFocusNode, + onChanged: (value) { + setState(() { + query = value; + }); + }, + decoration: InputDecoration( + hintText: widget.translations.searchPlaceholder, + hintStyle: theme.inputDecorationTheme.hintStyle ?? + const TextStyle( + color: Colors.white, + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: theme.inputDecorationTheme.focusedBorder?.borderSide + .color ?? + Colors.white, + ), + ), + ), + style: theme.inputDecorationTheme.hintStyle ?? + const TextStyle( + color: Colors.white, + ), + cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, + ), + ) + : Text( + widget.translations.newGroupChatButton, + style: theme.appBarTheme.titleTextStyle ?? + const TextStyle( + color: Colors.white, + ), + ); + } + + Widget _buildSearchIcon() { + var theme = Theme.of(context); + + return IconButton( + onPressed: () { + setState(() { + _isSearching = !_isSearching; + query = ''; + }); + + if (_isSearching) { + _textFieldFocusNode.requestFocus(); + } + }, + icon: Icon( + _isSearching ? Icons.close : Icons.search, + color: theme.appBarTheme.iconTheme?.color ?? Colors.white, + ), + ); + } + + Widget _buildUserList(List users) { + var filteredUsers = users + .where( + (user) => + user.fullName?.toLowerCase().contains( + query.toLowerCase(), + ) ?? + false, + ) + .toList(); + + if (filteredUsers.isEmpty) { + return widget.options.noChatsPlaceholderBuilder(widget.translations); + } + + return ListView.builder( + itemCount: filteredUsers.length, + itemBuilder: (context, index) { + var user = filteredUsers[index]; + var isSelected = selectedUserList.contains(user); + + return InkWell( + onTap: () { + setState(() { + if (isSelected) { + selectedUserList.remove(user); + } else { + selectedUserList.add(user); + } + debugPrint('The list of selected users is $selectedUserList'); + }); + }, + child: Container( + color: isSelected ? Colors.amber.shade200 : Colors.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: widget.options.chatRowContainerBuilder( + ChatRow( + avatar: widget.options.userAvatarBuilder( + user, + 40.0, + ), + title: user.fullName ?? widget.translations.anonymousUser, + ), + ), + ), + if (isSelected) + const Padding( + padding: EdgeInsets.only(right: 16.0), + child: Icon(Icons.check_circle, color: Colors.green), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/packages/flutter_chat_view/lib/src/services/profile_service.dart b/packages/flutter_chat_view/lib/src/services/profile_service.dart index 1ca431b..32b1dd7 100644 --- a/packages/flutter_chat_view/lib/src/services/profile_service.dart +++ b/packages/flutter_chat_view/lib/src/services/profile_service.dart @@ -22,4 +22,13 @@ class ChatProfileService extends ProfileService { }) { throw UnimplementedError(); } + + @override + FutureOr changePassword( + BuildContext context, + String currentPassword, + String newPassword, + ) { + throw UnimplementedError(); + } } diff --git a/packages/flutter_chat_view/pubspec.yaml b/packages/flutter_chat_view/pubspec.yaml index 64b0d7c..2bca5e8 100644 --- a/packages/flutter_chat_view/pubspec.yaml +++ b/packages/flutter_chat_view/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_chat_view description: A standard flutter package. -version: 1.2.1 +version: 1.4.0 publish_to: none @@ -20,15 +20,15 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_chat path: packages/flutter_chat_interface - ref: 1.2.1 + ref: 1.4.0 cached_network_image: ^3.2.2 flutter_image_picker: git: url: https://github.com/Iconica-Development/flutter_image_picker - ref: 1.0.4 + ref: 1.0.5 flutter_profile: git: - ref: 1.1.5 + ref: 1.3.0 url: https://github.com/Iconica-Development/flutter_profile dev_dependencies: