From 8f13d87a23e9cc281dca8a620729d75c53d80132 Mon Sep 17 00:00:00 2001 From: mike doornenbal Date: Wed, 17 Jul 2024 13:01:21 +0200 Subject: [PATCH] feat: ui update --- CHANGELOG.md | 3 + packages/flutter_chat/example/pubspec.yaml | 9 - .../src/flutter_chat_navigator_userstory.dart | 116 +++++-- .../lib/src/flutter_chat_userstory.dart | 49 ++- .../lib/src/models/chat_configuration.dart | 6 +- .../lib/dto/firebase_chat_document.dart | 7 +- .../service/firebase_chat_detail_service.dart | 1 - .../firebase_chat_overview_service.dart | 64 +++- packages/flutter_chat_firebase/pubspec.yaml | 2 +- .../lib/src/model/group_chat.dart | 13 +- .../src/service/chat_overview_service.dart | 21 +- packages/flutter_chat_interface/pubspec.yaml | 2 +- .../service/local_chat_overview_service.dart | 39 ++- .../lib/service/local_chat_user_service.dart | 2 +- packages/flutter_chat_local/pubspec.yaml | 2 +- .../lib/src/components/chat_detail_row.dart | 21 +- .../lib/src/components/chat_row.dart | 15 +- .../src/components/image_picker_popup.dart | 33 ++ .../lib/src/config/chat_options.dart | 143 ++++---- .../lib/src/config/chat_translations.dart | 65 +++- .../lib/src/screens/chat_detail_screen.dart | 35 +- .../lib/src/screens/chat_profile_screen.dart | 160 ++++++--- .../lib/src/screens/chat_screen.dart | 272 ++++++++------- .../lib/src/screens/new_chat_screen.dart | 147 +++----- .../new_group_chat_overview_screen.dart | 313 +++++++++++++++--- .../src/screens/new_group_chat_screen.dart | 226 ++++++++----- .../lib/src/services/date_formatter.dart | 36 +- packages/flutter_chat_view/pubspec.yaml | 4 +- 28 files changed, 1195 insertions(+), 611 deletions(-) create mode 100644 packages/flutter_chat_view/lib/src/components/image_picker_popup.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 253f34f..8bcd64c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 3.1.0 - Fix center the texts for no users found with search and type first message +- Fix styling for the whole userstory +- Add groupchat profile picture, and bio to the groupchat creation screen +- Updated profile of users and groups ## 3.0.1 diff --git a/packages/flutter_chat/example/pubspec.yaml b/packages/flutter_chat/example/pubspec.yaml index 6b4e754..09249cc 100644 --- a/packages/flutter_chat/example/pubspec.yaml +++ b/packages/flutter_chat/example/pubspec.yaml @@ -16,15 +16,6 @@ dependencies: path: ../ flutter_chat_firebase: path: ../../flutter_chat_firebase -dependency_overrides: - flutter_chat: - path: ../../flutter_chat - flutter_chat_interface: - path: ../../flutter_chat_interface - flutter_chat_local: - path: ../../flutter_chat_local - flutter_chat_view: - path: ../../flutter_chat_view dev_dependencies: flutter_test: 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 d3cead9..d7d76ff 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart @@ -104,16 +104,21 @@ Widget _chatDetailScreenRoute( if (configuration.onPressUserProfile != null) { return configuration.onPressUserProfile?.call(context, user); } - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _chatProfileScreenRoute( - configuration, - context, - chatId, - user.id, + var currentUser = + await configuration.chatService.chatUserService.getCurrentUser(); + var currentUserId = currentUser!.id!; + if (context.mounted) + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatProfileScreenRoute( + configuration, + context, + chatId, + user.id, + currentUserId, + ), ), - ), - ); + ); }, onMessageSubmit: (message) async { if (configuration.onMessageSubmit != null) { @@ -142,17 +147,21 @@ Widget _chatDetailScreenRoute( if (configuration.onPressChatTitle?.call(context, chat) != null) { return configuration.onPressChatTitle?.call(context, chat); } - - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _chatProfileScreenRoute( - configuration, - context, - chatId, - null, + var currentUser = + await configuration.chatService.chatUserService.getCurrentUser(); + var currentUserId = currentUser!.id!; + if (context.mounted) + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatProfileScreenRoute( + configuration, + context, + chatId, + null, + currentUserId, + ), ), - ), - ); + ); }, iconColor: configuration.iconColor, ); @@ -168,27 +177,63 @@ Widget _chatProfileScreenRoute( BuildContext context, String chatId, String? userId, + String currentUserId, ) => ChatProfileScreen( + options: configuration.chatOptionsBuilder(context), translations: configuration.translations, chatService: configuration.chatService, chatId: chatId, userId: userId, + currentUserId: currentUserId, onTapUser: (user) async { if (configuration.onPressUserProfile != null) { return configuration.onPressUserProfile!.call(context, user); } - - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _chatProfileScreenRoute( - configuration, - context, - chatId, - user.id, + var currentUser = + await configuration.chatService.chatUserService.getCurrentUser(); + var currentUserId = currentUser!.id!; + if (context.mounted) + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatProfileScreenRoute( + configuration, + context, + chatId, + user.id, + currentUserId, + ), ), - ), - ); + ); + }, + onPressStartChat: (user) async { + configuration.onPressCreateChat?.call(user); + if (configuration.onPressCreateChat != null) return; + var chat = await configuration.chatService.chatOverviewService + .getChatByUser(user); + if (chat.id == null) { + chat = await configuration.chatService.chatOverviewService + .storeChatIfNot( + PersonalChatModel( + user: user, + ), + null, + ); + } + if (context.mounted) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => PopScope( + canPop: false, + child: _chatDetailScreenRoute( + configuration, + context, + chat.id!, + ), + ), + ), + ); + } }, ); @@ -207,6 +252,8 @@ Widget _newChatScreenRoute( showGroupChatButton: configuration.enableGroupChatCreation, onPressCreateGroupChat: () async { configuration.onPressCreateGroupChat?.call(); + configuration.chatService.chatOverviewService + .clearCurrentlySelectedUsers(); if (context.mounted) { await Navigator.of(context).push( MaterialPageRoute( @@ -223,13 +270,13 @@ Widget _newChatScreenRoute( 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( PersonalChatModel( user: user, ), + null, ); } if (context.mounted) { @@ -277,19 +324,20 @@ Widget _newGroupChatOverviewScreenRoute( options: configuration.chatOptionsBuilder(context), translations: configuration.translations, service: configuration.chatService, - users: users, - onPressCompleteGroupChatCreation: (users, groupChatName) async { + onPressCompleteGroupChatCreation: + (users, groupChatName, groupBio, image) async { configuration.onPressCompleteGroupChatCreation - ?.call(users, groupChatName); + ?.call(users, groupChatName, image); if (configuration.onPressCreateGroupChat != null) return; var chat = await configuration.chatService.chatOverviewService.storeChatIfNot( GroupChatModel( canBeDeleted: true, title: groupChatName, - imageUrl: "https://picsum.photos/200/300", users: users, + bio: groupBio, ), + image, ); if (context.mounted) { await Navigator.of(context).pushReplacement( diff --git a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart b/packages/flutter_chat/lib/src/flutter_chat_userstory.dart index d81e752..0aedc8f 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_userstory.dart @@ -152,6 +152,7 @@ List getChatStoryRoutes( PersonalChatModel( user: user, ), + null, ); } if (context.mounted) { @@ -160,9 +161,13 @@ List getChatStoryRoutes( ); } }, - onPressCreateGroupChat: () async => context.push( - ChatUserStoryRoutes.newGroupChatScreen, - ), + onPressCreateGroupChat: () async { + configuration.chatService.chatOverviewService + .clearCurrentlySelectedUsers(); + return context.push( + ChatUserStoryRoutes.newGroupChatScreen, + ); + }, ); return buildScreenWithoutTransition( context: context, @@ -211,25 +216,25 @@ List getChatStoryRoutes( pageBuilder: (context, state) { var service = configuration.chatServiceBuilder?.call(context) ?? configuration.chatService; - var users = state.extra! as List; var newGroupChatOverviewScreen = NewGroupChatOverviewScreen( options: configuration.chatOptionsBuilder(context), translations: configuration.translationsBuilder?.call(context) ?? configuration.translations, service: service, - users: users, - onPressCompleteGroupChatCreation: (users, groupChatName) async { + onPressCompleteGroupChatCreation: + (users, groupChatName, groupBio, image) async { configuration.onPressCompleteGroupChatCreation - ?.call(users, groupChatName); + ?.call(users, groupChatName, image); var chat = await configuration.chatService.chatOverviewService .storeChatIfNot( GroupChatModel( canBeDeleted: true, title: groupChatName, - imageUrl: "https://picsum.photos/200/300", users: users, + bio: groupBio, ), + image, ); if (context.mounted) { context.go( @@ -259,13 +264,21 @@ List getChatStoryRoutes( var id = userId == "null" ? null : userId; var service = configuration.chatServiceBuilder?.call(context) ?? configuration.chatService; + ChatUserModel? currentUser; + String? currentUserId; + Future.delayed(Duration.zero, () async { + currentUser = await service.chatUserService.getCurrentUser(); + currentUserId = currentUser!.id; + }); var profileScreen = ChatProfileScreen( + options: configuration.chatOptionsBuilder(context), translations: configuration.translationsBuilder?.call(context) ?? configuration.translations, chatService: service, chatId: chatId!, userId: id, + currentUserId: currentUserId!, onTapUser: (user) async { if (configuration.onPressUserProfile != null) { return configuration.onPressUserProfile!.call(context, user); @@ -275,6 +288,26 @@ List getChatStoryRoutes( ChatUserStoryRoutes.chatProfileScreenPath(chatId, user.id), ); }, + onPressStartChat: (user) async { + configuration.onPressCreateChat?.call(user); + if (configuration.onPressCreateChat != null) return; + var chat = await configuration.chatService.chatOverviewService + .getChatByUser(user); + if (chat.id == null) { + chat = await configuration.chatService.chatOverviewService + .storeChatIfNot( + PersonalChatModel( + user: user, + ), + null, + ); + } + if (context.mounted) { + await context.push( + ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""), + ); + } + }, ); 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 8e00728..06b9c6f 100644 --- a/packages/flutter_chat/lib/src/models/chat_configuration.dart +++ b/packages/flutter_chat/lib/src/models/chat_configuration.dart @@ -83,7 +83,11 @@ class ChatUserStoryConfiguration { final Function(ChatUserModel)? onPressCreateChat; /// Builder for chat options based on context. - final Function(List, String)? onPressCompleteGroupChatCreation; + final Function( + List users, + String groupchatName, + Uint8List? image, + )? onPressCompleteGroupChatCreation; final Function()? onPressCreateGroupChat; diff --git a/packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart b/packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart index c04179a..721b67e 100644 --- a/packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart +++ b/packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart @@ -19,6 +19,7 @@ class FirebaseChatDocument { this.title, this.imageUrl, this.lastMessage, + this.bio, }); /// Constructs a FirebaseChatDocument from JSON. @@ -34,7 +35,8 @@ class FirebaseChatDocument { : FirebaseMessageDocument.fromJson( json["last_message"], null, - ); + ), + bio = json["bio"]; /// The unique identifier of the chat document. final String? id; @@ -60,6 +62,8 @@ class FirebaseChatDocument { /// The last message in the chat. final FirebaseMessageDocument? lastMessage; + final String? bio; + /// Converts the FirebaseChatDocument to JSON format. Map toJson() => { "title": title, @@ -68,5 +72,6 @@ class FirebaseChatDocument { "last_used": lastUsed, "can_be_deleted": canBeDeleted, "users": users, + "bio": bio, }; } 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 a14a121..3e9dad6 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 @@ -241,7 +241,6 @@ class FirebaseChatDetailService _cumulativeMessages = []; lastChat = chatId; lastMessage = null; - debugPrint("Canceling messages stream"); }, ); 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 81ee249..d4f531e 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 @@ -4,16 +4,20 @@ // SPDX-License-Identifier: BSD-3-Clause import "dart:async"; +import "dart:typed_data"; import "package:cloud_firestore/cloud_firestore.dart"; import "package:firebase_core/firebase_core.dart"; import "package:firebase_storage/firebase_storage.dart"; +import "package:flutter/material.dart"; import "package:flutter_chat_firebase/config/firebase_chat_options.dart"; import "package:flutter_chat_firebase/dto/firebase_chat_document.dart"; import "package:flutter_chat_interface/flutter_chat_interface.dart"; /// Service class for managing chat overviews using Firebase. -class FirebaseChatOverviewService implements ChatOverviewService { +class FirebaseChatOverviewService + with ChangeNotifier + implements ChatOverviewService { late FirebaseFirestore _db; late FirebaseStorage _storage; late ChatUserService _userService; @@ -38,6 +42,8 @@ class FirebaseChatOverviewService implements ChatOverviewService { _options = options ?? const FirebaseChatOptions(); } + final List _currentlySelectedUsers = []; + Future _addUnreadChatSubscription( String chatId, String userId, @@ -285,6 +291,7 @@ class FirebaseChatOverviewService implements ChatOverviewService { imageUrl: chat?.imageUrl ?? "", users: users, canBeDeleted: chat?.canBeDeleted ?? true, + bio: chat?.bio, ); } } @@ -338,7 +345,7 @@ class FirebaseChatOverviewService implements ChatOverviewService { /// /// [chat]: The chat to be stored. @override - Future storeChatIfNot(ChatModel chat) async { + Future storeChatIfNot(ChatModel chat, Uint8List? image) async { if (chat.id == null) { var currentUser = await _userService.getCurrentUser(); if (chat is PersonalChatModel) { @@ -398,22 +405,41 @@ class FirebaseChatOverviewService implements ChatOverviewService { FirebaseChatDocument( personal: false, title: chat.title, - imageUrl: chat.imageUrl, canBeDeleted: chat.canBeDeleted, users: userIds, lastUsed: Timestamp.now(), + bio: chat.bio, ), ); + if (image != null) { + var imageUrl = await uploadGroupChatImage(image, reference.id); + chat.copyWith(imageUrl: imageUrl); + await _db + .collection(_options.chatsMetaDataCollectionName) + .doc(reference.id) + .set({"image_url": imageUrl}, SetOptions(merge: true)); + } + var currentChat = await _db + .collection(_options.chatsMetaDataCollectionName) + .doc(reference.id) + .withConverter( + fromFirestore: (snapshot, _) => + FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id), + toFirestore: (chat, _) => chat.toJson(), + ) + .get(); + for (var userId in userIds) { await _db .collection(_options.usersCollectionName) .doc(userId) .collection(_options.groupChatsCollectionName) - .doc(reference.id) + .doc(currentChat.id) .set({"users": userIds}, SetOptions(merge: true)); } chat.id = reference.id; + currentlySelectedUsers.clear(); } else { throw Exception("Chat type not supported for firebase"); } @@ -478,4 +504,34 @@ class FirebaseChatOverviewService implements ChatOverviewService { .doc(chat.id) .set({"amount_unread_messages": 0}, SetOptions(merge: true)); } + + @override + List get currentlySelectedUsers => _currentlySelectedUsers; + + @override + void addCurrentlySelectedUser(ChatUserModel user) { + _currentlySelectedUsers.add(user); + notifyListeners(); + } + + @override + void removeCurrentlySelectedUser(ChatUserModel user) { + _currentlySelectedUsers.remove(user); + notifyListeners(); + } + + @override + Future uploadGroupChatImage(Uint8List image, String chatId) async { + await _storage.ref("groupchatImages/$chatId").putData(image); + var imageUrl = + await _storage.ref("groupchatImages/$chatId").getDownloadURL(); + + return imageUrl; + } + + @override + void clearCurrentlySelectedUsers() { + _currentlySelectedUsers.clear(); + notifyListeners(); + } } diff --git a/packages/flutter_chat_firebase/pubspec.yaml b/packages/flutter_chat_firebase/pubspec.yaml index 55e5aa1..2ca2ea4 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: 3.0.1 +version: 3.1.0 publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub 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 2c51d7e..84f56ca 100644 --- a/packages/flutter_chat_interface/lib/src/model/group_chat.dart +++ b/packages/flutter_chat_interface/lib/src/model/group_chat.dart @@ -16,8 +16,9 @@ abstract class GroupChatModelInterface extends ChatModel { }); String get title; - String get imageUrl; + String? get imageUrl; List get users; + String? get bio; @override GroupChatModelInterface copyWith({ @@ -30,6 +31,7 @@ abstract class GroupChatModelInterface extends ChatModel { String? imageUrl, List? users, bool? canBeDeleted, + String? bio, }); } @@ -56,13 +58,14 @@ class GroupChatModel implements GroupChatModelInterface { GroupChatModel({ required this.canBeDeleted, required this.title, - required this.imageUrl, required this.users, + this.imageUrl, this.id, this.messages, this.unreadMessages, this.lastUsed, this.lastMessage, + this.bio, }); @override @@ -80,9 +83,11 @@ class GroupChatModel implements GroupChatModelInterface { @override final String title; @override - final String imageUrl; + final String? imageUrl; @override final List users; + @override + final String? bio; @override GroupChatModel copyWith({ @@ -95,6 +100,7 @@ class GroupChatModel implements GroupChatModelInterface { String? title, String? imageUrl, List? users, + String? bio, }) => GroupChatModel( id: id ?? this.id, @@ -106,5 +112,6 @@ class GroupChatModel implements GroupChatModelInterface { title: title ?? this.title, imageUrl: imageUrl ?? this.imageUrl, users: users ?? this.users, + bio: bio ?? this.bio, ); } diff --git a/packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart b/packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart index 7329abf..effc9ea 100644 --- a/packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart +++ b/packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart @@ -1,6 +1,9 @@ +import "dart:typed_data"; + +import "package:flutter/material.dart"; import "package:flutter_chat_interface/flutter_chat_interface.dart"; -abstract class ChatOverviewService { +abstract class ChatOverviewService extends ChangeNotifier { /// Retrieves a stream of chats. /// This stream is updated whenever a new chat is created. Stream> getChatsStream(); @@ -18,8 +21,22 @@ abstract class ChatOverviewService { Future readChat(ChatModel chat); /// Creates the chat if it does not exist. - Future storeChatIfNot(ChatModel chat); + Future storeChatIfNot(ChatModel chat, Uint8List? image); /// Retrieves the number of unread chats. Stream getUnreadChatsCountStream(); + + /// Retrieves the currently selected users to be added to a new groupchat. + List get currentlySelectedUsers; + + /// Adds a user to the currently selected users. + void addCurrentlySelectedUser(ChatUserModel user); + + /// Deletes a user from the currently selected users. + void removeCurrentlySelectedUser(ChatUserModel user); + + void clearCurrentlySelectedUsers(); + + /// uploads an image for a group chat. + Future uploadGroupChatImage(Uint8List image, String chatId); } diff --git a/packages/flutter_chat_interface/pubspec.yaml b/packages/flutter_chat_interface/pubspec.yaml index 32a0f11..b4c452c 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: 3.0.1 +version: 3.1.0 publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub environment: 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 cf67089..0d5ec79 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 @@ -7,6 +7,9 @@ import "package:flutter_chat_interface/flutter_chat_interface.dart"; class LocalChatOverviewService with ChangeNotifier implements ChatOverviewService { + /// The list of currently selected users. + final List _currentlySelectedUsers = []; + /// The list of personal chat models. final List _chats = []; @@ -22,7 +25,6 @@ class LocalChatOverviewService _chats[index] = chat; _chatsController.addStream(Stream.value(_chats)); notifyListeners(); - debugPrint("Chat updated: $chat"); return Future.value(); } @@ -31,15 +33,12 @@ 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) { var chat = _chats.firstWhere((element) => element.id == id); - debugPrint("Retrieved chat by ID: $chat"); - debugPrint("Messages are: ${chat.messages?.length}"); return Future.value(chat); } @@ -59,7 +58,6 @@ class LocalChatOverviewService ); chat.id = chat.hashCode.toString(); _chats.add(chat); - debugPrint("New chat created: $chat"); } _chatsController.add([..._chats]); @@ -77,19 +75,42 @@ class LocalChatOverviewService Future readChat(ChatModel chat) async => Future.value(); @override - Future storeChatIfNot(ChatModel chat) { + Future storeChatIfNot(ChatModel chat, Uint8List? image) { var chatExists = _chats.any((element) => element.id == chat.id); if (!chatExists) { chat.id = chat.hashCode.toString(); _chats.add(chat); _chatsController.add([..._chats]); + currentlySelectedUsers.clear(); notifyListeners(); - debugPrint("Chat stored: $chat"); - } else { - debugPrint("Chat already exists: $chat"); } return Future.value(chat); } + + @override + List get currentlySelectedUsers => _currentlySelectedUsers; + + @override + void addCurrentlySelectedUser(ChatUserModel user) { + _currentlySelectedUsers.add(user); + notifyListeners(); + } + + @override + void removeCurrentlySelectedUser(ChatUserModel user) { + _currentlySelectedUsers.remove(user); + notifyListeners(); + } + + @override + Future uploadGroupChatImage(Uint8List image, String chatId) => + Future.value("https://picsum.photos/200/300"); + + @override + void clearCurrentlySelectedUsers() { + _currentlySelectedUsers.clear(); + notifyListeners(); + } } 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 932e9aa..34c4809 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 @@ -30,7 +30,7 @@ class LocalChatUserService implements ChatUserService { @override Future getCurrentUser() => - Future.value(const ChatUserModel()); + Future.value(users.where((element) => element.id == "3").first); @override Future getUser(String id) { diff --git a/packages/flutter_chat_local/pubspec.yaml b/packages/flutter_chat_local/pubspec.yaml index 40a8f0a..65e7c3c 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: 3.0.1 +version: 3.1.0 publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub 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 6355973..395bffd 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 @@ -103,11 +103,7 @@ class _ChatDetailRowState extends State { Text( widget.message.sender.fullName ?? widget.translations.anonymousUser, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: theme.textTheme.labelMedium?.color, - ), + style: theme.textTheme.titleMedium, ), ), Padding( @@ -115,13 +111,9 @@ class _ChatDetailRowState extends State { child: Text( _dateFormatter.format( date: widget.message.timestamp, - showFullDate: true, - ), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w300, - color: Color(0xFFBBBBBB), + showFullDate: false, ), + style: theme.textTheme.labelSmall, ), ), ], @@ -137,10 +129,7 @@ class _ChatDetailRowState extends State { Flexible( child: Text( (widget.message as ChatTextMessageModel).text, - style: TextStyle( - fontSize: 16, - color: theme.textTheme.labelMedium?.color, - ), + style: theme.textTheme.bodySmall, ), ), if (widget.showTime && @@ -155,7 +144,7 @@ class _ChatDetailRowState extends State { ) .split(" ") .last, - style: theme.textTheme.bodySmall, + style: theme.textTheme.labelSmall, textAlign: TextAlign.end, ), ], diff --git a/packages/flutter_chat_view/lib/src/components/chat_row.dart b/packages/flutter_chat_view/lib/src/components/chat_row.dart index 547f5b8..fd9414f 100644 --- a/packages/flutter_chat_view/lib/src/components/chat_row.dart +++ b/packages/flutter_chat_view/lib/src/components/chat_row.dart @@ -49,9 +49,7 @@ class ChatRow extends StatelessWidget { title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: unreadMessages > 0 - ? theme.textTheme.bodyLarge - : theme.textTheme.bodyMedium, + style: theme.textTheme.titleMedium, ), if (subTitle != null) ...[ Padding( @@ -59,8 +57,10 @@ class ChatRow extends StatelessWidget { child: Text( subTitle!, style: unreadMessages > 0 - ? theme.textTheme.bodyLarge - : theme.textTheme.bodyMedium, + ? theme.textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w800, + ) + : theme.textTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 2, ), @@ -79,10 +79,7 @@ class ChatRow extends StatelessWidget { padding: const EdgeInsets.only(bottom: 4.0), child: Text( lastUsed!, - style: const TextStyle( - color: Color(0xFFBBBBBB), - fontSize: 14, - ), + style: theme.textTheme.labelSmall, ), ), ], diff --git a/packages/flutter_chat_view/lib/src/components/image_picker_popup.dart b/packages/flutter_chat_view/lib/src/components/image_picker_popup.dart new file mode 100644 index 0000000..66f54a3 --- /dev/null +++ b/packages/flutter_chat_view/lib/src/components/image_picker_popup.dart @@ -0,0 +1,33 @@ +import "dart:typed_data"; + +import "package:flutter/material.dart"; +import "package:flutter_chat_view/flutter_chat_view.dart"; +import "package:flutter_chat_view/src/components/image_loading_snackbar.dart"; + +Future onPressSelectImage( + BuildContext context, + ChatTranslations translations, + ChatOptions options, + Function(Uint8List image) onUploadImage, +) async => + showModalBottomSheet( + context: context, + builder: (BuildContext context) => options.imagePickerContainerBuilder( + () => Navigator.of(context).pop(), + translations, + context, + ), + ).then( + (image) async { + if (image == null) return; + var messenger = ScaffoldMessenger.of(context) + ..showSnackBar( + getImageLoadingSnackbar(translations), + ) + ..activate(); + await onUploadImage(image); + Future.delayed(const Duration(seconds: 1), () { + messenger.hideCurrentSnackBar(); + }); + }, + ); 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 fcb7807..b055bca 100644 --- a/packages/flutter_chat_view/lib/src/config/chat_options.dart +++ b/packages/flutter_chat_view/lib/src/config/chat_options.dart @@ -4,7 +4,6 @@ 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"; @@ -57,31 +56,29 @@ Widget _createNewChatButton( BuildContext context, VoidCallback onPressed, ChatTranslations translations, -) => - Padding( - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: 5, - ), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - fixedSize: const Size(254, 44), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(56), - ), - ), - onPressed: onPressed, - child: Text( - translations.newChatButton, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w800, - fontSize: 20, - ), +) { + var theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 4, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + fixedSize: const Size(254, 44), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(56), ), ), - ); + onPressed: onPressed, + child: Text( + translations.newChatButton, + style: theme.textTheme.displayLarge, + ), + ), + ); +} Widget _createMessageInput( TextEditingController textEditingController, @@ -91,6 +88,7 @@ Widget _createMessageInput( ) { var theme = Theme.of(context); return TextField( + style: theme.textTheme.bodySmall, textCapitalization: TextCapitalization.sentences, controller: textEditingController, decoration: InputDecoration( @@ -111,7 +109,9 @@ Widget _createMessageInput( horizontal: 30, ), hintText: translations.messagePlaceholder, - hintStyle: theme.inputDecorationTheme.hintStyle, + hintStyle: theme.textTheme.bodyMedium!.copyWith( + color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5), + ), fillColor: Colors.white, filled: true, border: const OutlineInputBorder( @@ -127,47 +127,60 @@ Widget _createMessageInput( Widget _createChatRowContainer( Widget chatRow, -) => - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, - horizontal: 10.0, + BuildContext context, +) { + var theme = Theme.of(context); + return DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + width: 0.5, + ), ), - child: ColoredBox( - color: Colors.transparent, - child: chatRow, - ), - ); + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: chatRow, + ), + ); +} Widget _createImagePickerContainer( VoidCallback onClose, ChatTranslations translations, BuildContext context, -) => - Container( - padding: const EdgeInsets.all(8.0), - color: Colors.white, - child: ImagePicker( - imagePickerTheme: ImagePickerTheme( - title: translations.imagePickerTitle, - titleTextSize: 16, - titleAlignment: TextAlign.center, - iconSize: 60.0, - makePhotoText: translations.takePicture, - selectImageText: translations.uploadFile, +) { + var theme = Theme.of(context); + return Container( + padding: const EdgeInsets.all(8.0), + color: Colors.white, + child: ImagePicker( + imagePickerTheme: ImagePickerTheme( + title: translations.imagePickerTitle, + titleTextSize: 16, + titleAlignment: TextAlign.center, + iconSize: 60.0, + makePhotoText: translations.takePicture, + selectImageText: translations.uploadFile, + selectImageIcon: const Icon( + Icons.insert_drive_file_rounded, + size: 60, ), - customButton: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - ), - onPressed: onClose, - child: Text( - translations.cancelImagePickerBtn, - style: const TextStyle(color: Colors.white), + ), + customButton: TextButton( + onPressed: onClose, + child: Text( + translations.cancelImagePickerBtn, + style: theme.textTheme.bodyMedium!.copyWith( + decoration: TextDecoration.underline, ), ), ), - ); + ), + ); +} Scaffold _createScaffold( AppBar appbar, @@ -185,20 +198,27 @@ Widget _createUserAvatar( double size, ) => Avatar( + boxfit: BoxFit.cover, user: User( firstName: user.firstName, lastName: user.lastName, - imageUrl: user.imageUrl, + imageUrl: user.imageUrl != "" ? user.imageUrl : null, ), size: size, ); + Widget _createGroupAvatar( String groupName, - String imageUrl, + String? imageUrl, double size, ) => - ChatImage( - image: imageUrl, + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: groupName, + lastName: null, + imageUrl: imageUrl != "" ? imageUrl : null, + ), size: size, ); @@ -249,6 +269,7 @@ typedef TextInputBuilder = Widget Function( typedef ContainerBuilder = Widget Function( Widget child, + BuildContext context, ); typedef ImagePickerContainerBuilder = Widget Function( @@ -270,7 +291,7 @@ typedef UserAvatarBuilder = Widget Function( typedef GroupAvatarBuilder = Widget Function( String groupName, - String imageUrl, + String? imageUrl, double size, ); 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 6f63ff3..6c3ee2d 100644 --- a/packages/flutter_chat_view/lib/src/config/chat_translations.dart +++ b/packages/flutter_chat_view/lib/src/config/chat_translations.dart @@ -15,7 +15,6 @@ class ChatTranslations { required this.newChatButton, required this.newGroupChatButton, required this.newChatTitle, - required this.deleteChatButton, required this.image, required this.searchPlaceholder, required this.startTyping, @@ -40,6 +39,13 @@ class ChatTranslations { required this.groupNameValidatorTooLong, required this.groupNameHintText, required this.newGroupChatTitle, + required this.groupBioHintText, + required this.groupProfileBioHeader, + required this.groupBioValidatorEmpty, + required this.groupChatNameFieldHeader, + required this.groupBioFieldHeader, + required this.selectedMembersHeader, + required this.createGroupChatButton, }); /// Default translations for the chat component view @@ -47,7 +53,7 @@ class ChatTranslations { this.chatsTitle = "Chats", this.chatsUnread = "unread", this.newChatButton = "Start chat", - this.newGroupChatButton = "Create a group chat", + this.newGroupChatButton = "Create a groupchat", this.newChatTitle = "Start a chat", this.image = "Image", this.searchPlaceholder = "Search...", @@ -58,25 +64,31 @@ class ChatTranslations { this.writeFirstMessageInGroupChat = "Write the first message in this group chat", this.imageUploading = "Image is uploading...", - this.deleteChatButton = "Delete", this.deleteChatModalTitle = "Delete chat", this.deleteChatModalDescription = "Are you sure you want to delete this chat?", this.deleteChatModalCancel = "Cancel", - this.deleteChatModalConfirm = "Delete", + this.deleteChatModalConfirm = "Confirm", this.noUsersFound = "No users were found to start a chat with", this.noChatsFound = "Click on 'Start a chat' to create a new chat", this.anonymousUser = "Anonymous user", this.chatCantBeDeleted = "This chat can't be deleted", - this.chatProfileUsers = "Users:", + this.chatProfileUsers = "Members:", this.imagePickerTitle = "Do you want to upload a file or take a picture?", this.uploadFile = "UPLOAD FILE", this.takePicture = "TAKE PICTURE", - this.groupNameHintText = "Group chat name", + this.groupNameHintText = "Groupchat name", this.groupNameValidatorEmpty = "Please enter a group chat name", this.groupNameValidatorTooLong = "Group name is too long, max 15 characters", - this.newGroupChatTitle = "New Group Chat", + this.newGroupChatTitle = "start a groupchat", + this.groupBioHintText = "Bio", + this.groupProfileBioHeader = "Bio", + this.groupBioValidatorEmpty = "Please enter a bio", + this.groupChatNameFieldHeader = "Chat name", + this.groupBioFieldHeader = "Additional information for members", + this.selectedMembersHeader = "Members: ", + this.createGroupChatButton = "Create groupchat", }); final String chatsTitle; @@ -84,7 +96,6 @@ class ChatTranslations { final String newChatButton; final String newGroupChatButton; final String newChatTitle; - final String deleteChatButton; final String image; final String searchPlaceholder; final String startTyping; @@ -104,6 +115,10 @@ class ChatTranslations { final String imagePickerTitle; final String uploadFile; final String takePicture; + final String groupChatNameFieldHeader; + final String groupBioFieldHeader; + final String selectedMembersHeader; + final String createGroupChatButton; /// Shown when the user has no name final String anonymousUser; @@ -111,6 +126,9 @@ class ChatTranslations { final String groupNameValidatorTooLong; final String groupNameHintText; final String newGroupChatTitle; + final String groupBioHintText; + final String groupProfileBioHeader; + final String groupBioValidatorEmpty; // copyWith method to override the default values ChatTranslations copyWith({ @@ -119,7 +137,6 @@ class ChatTranslations { String? newChatButton, String? newGroupChatButton, String? newChatTitle, - String? deleteChatButton, String? image, String? searchPlaceholder, String? startTyping, @@ -144,6 +161,13 @@ class ChatTranslations { String? groupNameValidatorTooLong, String? groupNameHintText, String? newGroupChatTitle, + String? groupBioHintText, + String? groupProfileBioHeader, + String? groupBioValidatorEmpty, + String? groupChatNameFieldHeader, + String? groupBioFieldHeader, + String? selectedMembersHeader, + String? createGroupChatButton, }) => ChatTranslations( chatsTitle: chatsTitle ?? this.chatsTitle, @@ -151,7 +175,6 @@ class ChatTranslations { newChatButton: newChatButton ?? this.newChatButton, newGroupChatButton: newGroupChatButton ?? this.newGroupChatButton, newChatTitle: newChatTitle ?? this.newChatTitle, - deleteChatButton: deleteChatButton ?? this.deleteChatButton, image: image ?? this.image, searchPlaceholder: searchPlaceholder ?? this.searchPlaceholder, startTyping: startTyping ?? this.startTyping, @@ -177,9 +200,23 @@ class ChatTranslations { uploadFile: uploadFile ?? this.uploadFile, takePicture: takePicture ?? this.takePicture, anonymousUser: anonymousUser ?? this.anonymousUser, - groupNameValidatorEmpty: this.groupNameValidatorEmpty, - groupNameValidatorTooLong: this.groupNameValidatorTooLong, - groupNameHintText: this.groupNameHintText, - newGroupChatTitle: this.newGroupChatTitle, + groupNameValidatorEmpty: + groupNameValidatorEmpty ?? this.groupNameValidatorEmpty, + groupNameValidatorTooLong: + groupNameValidatorTooLong ?? this.groupNameValidatorTooLong, + groupNameHintText: groupNameHintText ?? this.groupNameHintText, + newGroupChatTitle: newGroupChatTitle ?? this.newGroupChatTitle, + groupBioHintText: groupBioHintText ?? this.groupBioHintText, + groupProfileBioHeader: + groupProfileBioHeader ?? this.groupProfileBioHeader, + groupBioValidatorEmpty: + groupBioValidatorEmpty ?? this.groupBioValidatorEmpty, + groupChatNameFieldHeader: + groupChatNameFieldHeader ?? this.groupChatNameFieldHeader, + groupBioFieldHeader: groupBioFieldHeader ?? this.groupBioFieldHeader, + selectedMembersHeader: + selectedMembersHeader ?? this.selectedMembersHeader, + createGroupChatButton: + createGroupChatButton ?? this.createGroupChatButton, ); } 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 5ae9d88..5241092 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 @@ -9,7 +9,7 @@ import "package:flutter/material.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"; -import "package:flutter_chat_view/src/components/image_loading_snackbar.dart"; +import "package:flutter_chat_view/src/components/image_picker_popup.dart"; class ChatDetailScreen extends StatefulWidget { const ChatDetailScreen({ @@ -139,29 +139,6 @@ class _ChatDetailScreenState extends State { Widget build(BuildContext context) { var theme = Theme.of(context); - Future onPressSelectImage() async => showModalBottomSheet( - context: context, - builder: (BuildContext context) => - widget.options.imagePickerContainerBuilder( - () => Navigator.of(context).pop(), - widget.translations, - context, - ), - ).then( - (image) async { - if (image == null) return; - var messenger = ScaffoldMessenger.of(context) - ..showSnackBar( - getImageLoadingSnackbar(widget.translations), - ) - ..activate(); - await widget.onUploadImage(image); - Future.delayed(const Duration(seconds: 1), () { - messenger.hideCurrentSnackBar(); - }); - }, - ); - return FutureBuilder( // ignore: discarded_futures future: widget.service.chatOverviewService.getChatById(widget.chatId), @@ -175,7 +152,6 @@ class _ChatDetailScreenState extends State { return Scaffold( appBar: AppBar( - backgroundColor: theme.appBarTheme.backgroundColor, iconTheme: theme.appBarTheme.iconTheme ?? const IconThemeData(color: Colors.white), centerTitle: true, @@ -192,7 +168,7 @@ class _ChatDetailScreenState extends State { child: widget.chatTitleBuilder?.call(chatTitle) ?? Text( chatTitle, - style: theme.appBarTheme.titleTextStyle, + style: theme.textTheme.headlineLarge, overflow: TextOverflow.ellipsis, ), ), @@ -253,7 +229,12 @@ class _ChatDetailScreenState extends State { ChatBottom( chat: chatModel, messageInputBuilder: widget.options.messageInputBuilder, - onPressSelectImage: onPressSelectImage, + onPressSelectImage: () async => onPressSelectImage.call( + context, + widget.translations, + widget.options, + widget.onUploadImage, + ), 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 e39f59c..af87290 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 @@ -8,6 +8,9 @@ class ChatProfileScreen extends StatefulWidget { required this.chatId, required this.translations, required this.onTapUser, + required this.options, + required this.onPressStartChat, + required this.currentUserId, this.userId, super.key, }); @@ -27,6 +30,15 @@ class ChatProfileScreen extends StatefulWidget { /// Callback function for tapping on a user. final Function(ChatUserModel user) onTapUser; + /// Chat options. + final ChatOptions options; + + /// Callback function for starting a chat. + final Function(ChatUserModel user) onPressStartChat; + + /// The current user. + final String currentUserId; + @override State createState() => _ProfileScreenState(); } @@ -65,9 +77,9 @@ class _ProfileScreenState extends State { imageUrl: data.imageUrl, ); } + return Scaffold( appBar: AppBar( - backgroundColor: theme.appBarTheme.backgroundColor, iconTheme: theme.appBarTheme.iconTheme ?? const IconThemeData(color: Colors.white), title: Text( @@ -78,63 +90,123 @@ class _ProfileScreenState extends State { : (data is GroupChatModel) ? data.title : "", - style: theme.appBarTheme.titleTextStyle, + style: theme.textTheme.headlineLarge, ), ), body: snapshot.hasData - ? ListView( + ? Stack( children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Avatar( - user: user, - ), - ), - const Divider( - color: Colors.white, - thickness: 10, - ), - if (data is GroupChatModel) ...[ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 100, - vertical: 20, - ), - child: Text( - widget.translations.chatProfileUsers, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + widget.options.userAvatarBuilder( + ChatUserModel( + firstName: user!.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl, + ), + 60, + ), + ], ), ), - ), - ...data.users.map((e) { - var user = User( - firstName: e.firstName ?? "", - lastName: e.lastName ?? "", - imageUrl: e.imageUrl, - ); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: GestureDetector( - onTap: () { - widget.onTapUser.call(e); - }, + const Divider( + color: Colors.white, + thickness: 10, + ), + if (data is GroupChatModel) ...[ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 20, + ), child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Avatar( - user: user, + Text( + widget.translations.groupProfileBioHeader, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, ), Text( - user.firstName!, + data.bio ?? "", + style: theme.textTheme.bodyMedium! + .copyWith(color: Colors.black), + ), + const SizedBox( + height: 12, + ), + Text( + widget.translations.chatProfileUsers, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + Wrap( + children: [ + ...data.users.map( + (user) => Padding( + padding: const EdgeInsets.only( + bottom: 8, + right: 8, + ), + child: GestureDetector( + onTap: () { + widget.onTapUser.call(user); + }, + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + widget.options.userAvatarBuilder( + user, + 44, + ), + ], + ), + ), + ), + ), + ], ), ], ), ), - ); - }), + ], + ], + ), + if (data is ChatUserModel && + widget.currentUserId != data.id) ...[ + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 80, + ), + child: FilledButton( + onPressed: () { + widget.onPressStartChat(data); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.translations.newChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ), ], ], ) 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 5393e13..fefa53c 100644 --- a/packages/flutter_chat_view/lib/src/screens/chat_screen.dart +++ b/packages/flutter_chat_view/lib/src/screens/chat_screen.dart @@ -77,10 +77,9 @@ class _ChatScreenState extends State { var theme = Theme.of(context); return widget.options.scaffoldBuilder( AppBar( - backgroundColor: theme.appBarTheme.backgroundColor, title: Text( translations.chatsTitle, - style: theme.appBarTheme.titleTextStyle, + style: theme.textTheme.headlineLarge, ), centerTitle: true, actions: [ @@ -96,9 +95,8 @@ class _ChatScreenState extends State { child: Text( "${snapshot.data ?? 0} ${translations.chatsUnread}", style: widget.unreadMessageTextStyle ?? - const TextStyle( + theme.textTheme.bodySmall!.copyWith( color: Colors.white, - fontSize: 14, ), ), ), @@ -147,154 +145,72 @@ class _ChatScreenState extends State { decoration: BoxDecoration( border: Border( bottom: BorderSide( - color: theme.colorScheme.secondary - .withOpacity(0.3), + color: theme.dividerColor, width: 0.5, ), ), ), child: Builder( - builder: (context) => !(widget - .disableDismissForPermanentChats && - !chat.canBeDeleted) - ? Dismissible( - confirmDismiss: (_) async => - widget.deleteChatDialog - ?.call(context, chat) ?? - showModalBottomSheet( - context: context, - builder: (BuildContext context) => - Container( - padding: - const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - chat.canBeDeleted - ? translations - .deleteChatModalTitle - : translations - .chatCantBeDeleted, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 24, - fontWeight: - FontWeight.bold, - ), - ), - const SizedBox(height: 24), - if (chat.canBeDeleted) - Padding( - padding: const EdgeInsets - .symmetric( - horizontal: 16, - ), - child: Text( - translations - .deleteChatModalDescription, - textAlign: - TextAlign.center, - style: const TextStyle( - fontSize: 18, - ), - ), - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - ElevatedButton( - onPressed: () => - Navigator.of( - context, - ).pop(false), - child: Text( - translations - .deleteChatModalCancel, - style: - const TextStyle( - color: Colors.black, - fontSize: 18, - ), - ), - ), - if (chat.canBeDeleted) - const SizedBox( - width: 16, - ), - if (chat.canBeDeleted) - ElevatedButton( - style: ElevatedButton - .styleFrom( - backgroundColor: - Theme.of( - context, - ).primaryColor, - ), - onPressed: () => - Navigator.of( - context, - ).pop( - true, - ), - child: Text( - translations - .deleteChatModalConfirm, - style: - const TextStyle( - color: - Colors.white, - fontSize: 18, - ), - ), - ), - ], - ), - ], + builder: (context) => + !(widget.disableDismissForPermanentChats && + !chat.canBeDeleted) + ? Dismissible( + confirmDismiss: (_) async => + widget.deleteChatDialog + ?.call(context, chat) ?? + _deleteDialog( + chat, + translations, + context, + ), + onDismissed: (_) { + setState(() { + deletedChats.add(chat.id!); + }); + widget.onDeleteChat(chat); + }, + secondaryBackground: const ColoredBox( + color: Colors.red, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.delete, + color: Colors.white, + ), ), ), ), - onDismissed: (_) { - setState(() { - deletedChats.add(chat.id!); - }); - widget.onDeleteChat(chat); - }, - background: ColoredBox( - color: Colors.red, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - translations.deleteChatButton, + background: const ColoredBox( + color: Colors.red, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.delete, + color: Colors.white, + ), + ), ), ), + key: ValueKey( + chat.id.toString(), + ), + child: ChatListItem( + widget: widget, + chat: chat, + translations: translations, + dateFormatter: _dateFormatter, + ), + ) + : ChatListItem( + widget: widget, + chat: chat, + translations: translations, + dateFormatter: _dateFormatter, ), - ), - key: ValueKey( - chat.id.toString(), - ), - child: ChatListItem( - widget: widget, - chat: chat, - translations: translations, - dateFormatter: _dateFormatter, - ), - ) - : ChatListItem( - widget: widget, - chat: chat, - translations: translations, - dateFormatter: _dateFormatter, - ), ), ), ], @@ -318,6 +234,79 @@ class _ChatScreenState extends State { theme.colorScheme.surface, ); } + + Future _deleteDialog( + ChatModel chat, + ChatTranslations translations, + BuildContext context, + ) async { + var theme = Theme.of(context); + var title = chat.canBeDeleted + ? translations.deleteChatModalTitle + : translations.chatCantBeDeleted; + return showModalBottomSheet( + context: context, + builder: (BuildContext context) => Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 20, + ), + if (chat.canBeDeleted) ...[ + Text( + translations.deleteChatModalDescription, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium, + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 60), + child: FilledButton( + onPressed: () { + Navigator.of( + context, + ).pop(true); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.translations.deleteChatModalConfirm, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ], + TextButton( + onPressed: () { + Navigator.of( + context, + ).pop(false); + }, + child: Text( + widget.translations.deleteChatModalCancel, + style: theme.textTheme.bodyMedium!.copyWith( + color: theme.textTheme.bodyMedium!.color?.withOpacity(0.5), + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + ), + ); + } } class ChatListItem extends StatelessWidget { @@ -381,6 +370,7 @@ class ChatListItem extends StatelessWidget { ) : null, ), + context, ), ); } 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 0ac64c2..54d68ca 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 @@ -50,7 +50,6 @@ class _NewChatScreenState extends State { appBar: AppBar( iconTheme: theme.appBarTheme.iconTheme ?? const IconThemeData(color: Colors.white), - backgroundColor: theme.appBarTheme.backgroundColor, title: _buildSearchField(), actions: [ _buildSearchIcon(), @@ -58,46 +57,31 @@ class _NewChatScreenState extends State { ), body: Column( children: [ - if (widget.showGroupChatButton) ...[ - GestureDetector( - onTap: () async { - await widget.onPressCreateGroupChat(); - }, - 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 - }, - ), - ), - Text( - widget.translations.newGroupChatButton, - style: const TextStyle( - color: Colors.white, - fontSize: 16.0, - ), - ), - ], - ), - ], - ), + if (widget.showGroupChatButton && !_isSearching) ...[ + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 20, + ), + child: FilledButton( + onPressed: () async { + await widget.onPressCreateGroupChat(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.groups, + ), + const SizedBox( + width: 4, + ), + Text( + widget.translations.newGroupChatButton, + style: theme.textTheme.displayLarge, + ), + ], ), ), ), @@ -138,19 +122,20 @@ class _NewChatScreenState extends State { }, decoration: InputDecoration( hintText: widget.translations.searchPlaceholder, - hintStyle: theme.inputDecorationTheme.hintStyle, + hintStyle: + theme.textTheme.bodyMedium!.copyWith(color: Colors.white), focusedBorder: UnderlineInputBorder( borderSide: BorderSide( color: theme.colorScheme.primary, ), ), ), - style: theme.inputDecorationTheme.hintStyle, + style: theme.textTheme.bodySmall!.copyWith(color: Colors.white), cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, ) : Text( widget.translations.newChatTitle, - style: theme.appBarTheme.titleTextStyle, + style: theme.textTheme.headlineLarge, ); } @@ -205,51 +190,14 @@ class _NewChatScreenState extends State { .noUsersPlaceholderBuilder(widget.translations, context); } var isPressed = false; - return ListView.builder( - itemCount: filteredUsers.length, - itemBuilder: (context, index) { - var user = filteredUsers[index]; - return DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.colorScheme.secondary.withOpacity(0.3), - width: 0.5, - ), - ), - ), - child: GestureDetector( - child: widget.options.chatRowContainerBuilder( - Padding( - padding: widget.options.paddingAroundChatList ?? - const EdgeInsets.symmetric(vertical: 8, horizontal: 28), - child: ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: widget.options.userAvatarBuilder(user, 40.0), - ), - Expanded( - child: Container( - height: 40.0, - alignment: Alignment.centerLeft, - child: Text( - user.fullName ?? - widget.translations.anonymousUser, - style: theme.textTheme.bodyLarge, - ), - ), - ), - ], - ), - ), - ), - ), - ), + return Padding( + padding: widget.options.paddingAroundChatList ?? + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: ListView.builder( + itemCount: filteredUsers.length, + itemBuilder: (context, index) { + var user = filteredUsers[index]; + return InkWell( onTap: () async { if (!isPressed) { isPressed = true; @@ -257,9 +205,24 @@ class _NewChatScreenState extends State { isPressed = false; } }, - ), - ); - }, + child: widget.options.chatRowContainerBuilder( + Row( + children: [ + widget.options.userAvatarBuilder(user, 44), + const SizedBox( + width: 12, + ), + Text( + user.fullName ?? widget.translations.anonymousUser, + style: theme.textTheme.titleMedium, + ), + ], + ), + context, + ), + ); + }, + ), ); } } 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 index 84a766e..5c6dbb2 100644 --- 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 @@ -2,15 +2,17 @@ // // SPDX-License-Identifier: BSD-3-Clause +import "dart:typed_data"; + import "package:flutter/material.dart"; import "package:flutter_chat_view/flutter_chat_view.dart"; +import "package:flutter_chat_view/src/components/image_picker_popup.dart"; class NewGroupChatOverviewScreen extends StatefulWidget { const NewGroupChatOverviewScreen({ required this.options, required this.onPressCompleteGroupChatCreation, required this.service, - required this.users, this.translations = const ChatTranslations.empty(), super.key, }); @@ -18,8 +20,12 @@ class NewGroupChatOverviewScreen extends StatefulWidget { final ChatOptions options; final ChatTranslations translations; final ChatService service; - final List users; - final Function(List, String) onPressCompleteGroupChatCreation; + final Function( + List users, + String groupchatName, + String? groupchatBio, + Uint8List? imageBytes, + ) onPressCompleteGroupChatCreation; @override State createState() => @@ -28,15 +34,24 @@ class NewGroupChatOverviewScreen extends StatefulWidget { class _NewGroupChatOverviewScreenState extends State { - final TextEditingController _textEditingController = TextEditingController(); + final TextEditingController _chatNameController = TextEditingController(); + final TextEditingController _bioController = TextEditingController(); + Uint8List? image; @override Widget build(BuildContext context) { var theme = Theme.of(context); var formKey = GlobalKey(); var isPressed = false; + var users = widget.service.chatOverviewService.currentlySelectedUsers; + + void onUploadImage(groupImage) { + setState(() { + image = groupImage; + }); + } + return Scaffold( - backgroundColor: theme.colorScheme.surface, appBar: AppBar( iconTheme: theme.appBarTheme.iconTheme ?? const IconThemeData(color: Colors.white), @@ -46,46 +61,260 @@ class _NewGroupChatOverviewScreenState style: theme.appBarTheme.titleTextStyle, ), ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: formKey, - child: TextFormField( - controller: _textEditingController, - decoration: InputDecoration( - hintText: widget.translations.groupNameHintText, - hintStyle: theme.inputDecorationTheme.hintStyle, + body: Stack( + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 40, + ), + Center( + child: Stack( + children: [ + GestureDetector( + onTap: () async { + await onPressSelectImage( + context, + widget.translations, + widget.options, + onUploadImage, + ); + }, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: const Color(0xFFD9D9D9), + borderRadius: BorderRadius.circular(40), + image: image != null + ? DecorationImage( + image: MemoryImage(image!), + fit: BoxFit.cover, + ) + : null, + ), + child: image == null + ? const Icon(Icons.image) + : null, + ), + ), + if (image != null) + Positioned.directional( + textDirection: Directionality.of(context), + end: 0, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: const Color(0xFFBCBCBC), + borderRadius: BorderRadius.circular(40), + ), + child: Center( + child: GestureDetector( + onTap: () { + setState(() { + image = null; + }); + }, + child: const Icon( + Icons.close, + size: 12, + ), + ), + ), + ), + ) + else + const SizedBox.shrink(), + ], + ), + ), + const SizedBox( + height: 40, + ), + Text( + widget.translations.groupChatNameFieldHeader, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + TextFormField( + style: theme.textTheme.bodySmall, + controller: _chatNameController, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + hintText: widget.translations.groupNameHintText, + hintStyle: theme.textTheme.bodyMedium!.copyWith( + color: theme.textTheme.bodyMedium!.color! + .withOpacity(0.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return widget.translations.groupNameValidatorEmpty; + } + if (value.length > 15) + return widget.translations.groupNameValidatorTooLong; + return null; + }, + ), + const SizedBox( + height: 16, + ), + Text( + widget.translations.groupBioFieldHeader, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + TextFormField( + style: theme.textTheme.bodySmall, + controller: _bioController, + minLines: null, + maxLines: 5, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + hintText: widget.translations.groupBioHintText, + hintStyle: theme.textTheme.bodyMedium!.copyWith( + color: theme.textTheme.bodyMedium!.color! + .withOpacity(0.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return widget.translations.groupBioValidatorEmpty; + } + + return null; + }, + ), + const SizedBox( + height: 16, + ), + Text( + "${widget.translations.selectedMembersHeader}" + "${users.length}", + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + Wrap( + children: [ + ...users.map( + _selectedUser, + ), + ], + ), + const SizedBox( + height: 80, + ), + ], + ), + ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return widget.translations.groupNameValidatorEmpty; - } - if (value.length > 15) - return widget.translations.groupNameValidatorTooLong; - return null; - }, ), - ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 80, + ), + child: FilledButton( + onPressed: users.isNotEmpty + ? () async { + if (!isPressed) { + isPressed = true; + if (formKey.currentState!.validate()) { + await widget.onPressCompleteGroupChatCreation( + users, + _chatNameController.text, + _bioController.text, + image, + ); + } + isPressed = false; + } + } + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.translations.createGroupChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ), + ], ), - floatingActionButton: FloatingActionButton( - backgroundColor: Theme.of(context).primaryColor, - onPressed: () async { - if (!isPressed) { - isPressed = true; - if (formKey.currentState!.validate()) { - await widget.onPressCompleteGroupChatCreation( - widget.users, - _textEditingController.text, - ); - } - isPressed = false; - } - }, - child: const Icon( - Icons.check_circle, - ), - ), - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + // floatingActionButton: FloatingActionButton( ); } + + Widget _selectedUser(ChatUserModel user) => GestureDetector( + onTap: () { + setState(() { + widget.service.chatOverviewService + .removeCurrentlySelectedUser(user); + }); + }, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: widget.options.userAvatarBuilder( + user, + 40, + ), + ), + Positioned.directional( + textDirection: Directionality.of(context), + end: 0, + child: const Icon( + Icons.cancel, + size: 20, + ), + ), + ], + ), + ); } 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 index 8fcb8c0..ddd34b3 100644 --- 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 @@ -23,7 +23,6 @@ class NewGroupChatScreen extends StatefulWidget { class _NewGroupChatScreenState extends State { final FocusNode _textFieldFocusNode = FocusNode(); - List selectedUserList = []; bool _isSearching = false; String query = ""; @@ -51,19 +50,19 @@ class _NewGroupChatScreenState extends State { } else if (snapshot.hasError) { return Text("Error: ${snapshot.error}"); } else if (snapshot.hasData) { - return _buildUserList(snapshot.data!); + return Stack( + children: [ + _buildUserList(snapshot.data!), + NextButton( + service: widget.service, + onPressGroupChatOverview: widget.onPressGroupChatOverview, + ), + ], + ); } return const SizedBox.shrink(); }, ), - floatingActionButton: FloatingActionButton( - backgroundColor: Theme.of(context).primaryColor, - onPressed: () async { - await widget.onPressGroupChatOverview(selectedUserList); - }, - child: const Icon(Icons.arrow_forward_ios), - ), - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } @@ -80,14 +79,15 @@ class _NewGroupChatScreenState extends State { }, decoration: InputDecoration( hintText: widget.translations.searchPlaceholder, - hintStyle: theme.inputDecorationTheme.hintStyle, + hintStyle: + theme.textTheme.bodyMedium!.copyWith(color: Colors.white), focusedBorder: UnderlineInputBorder( borderSide: BorderSide( color: theme.colorScheme.primary, ), ), ), - style: theme.inputDecorationTheme.hintStyle, + style: theme.textTheme.bodySmall!.copyWith(color: Colors.white), cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, ) : Text( @@ -140,9 +140,74 @@ class _NewGroupChatScreenState extends State { return UserList( filteredUsers: filteredUsers, - selectedUserList: selectedUserList, options: widget.options, translations: widget.translations, + service: widget.service, + ); + } +} + +class NextButton extends StatefulWidget { + const NextButton({ + required this.service, + required this.onPressGroupChatOverview, + super.key, + }); + + final ChatService service; + final Function(List) onPressGroupChatOverview; + + @override + State createState() => _NextButtonState(); +} + +class _NextButtonState extends State { + @override + void initState() { + widget.service.chatOverviewService.addListener(_listen); + super.initState(); + } + + @override + void dispose() { + widget.service.chatOverviewService.removeListener(_listen); + super.dispose(); + } + + void _listen() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 80, + ), + child: FilledButton( + onPressed: widget + .service.chatOverviewService.currentlySelectedUsers.isNotEmpty + ? () async { + await widget.onPressGroupChatOverview( + widget.service.chatOverviewService.currentlySelectedUsers, + ); + } + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Next", + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), ); } } @@ -150,16 +215,16 @@ class _NewGroupChatScreenState extends State { class UserList extends StatefulWidget { const UserList({ required this.filteredUsers, - required this.selectedUserList, required this.options, required this.translations, + required this.service, super.key, }); final List filteredUsers; - final List selectedUserList; final ChatOptions options; final ChatTranslations translations; + final ChatService service; @override State createState() => _UserListState(); @@ -167,75 +232,70 @@ class UserList extends StatefulWidget { class _UserListState extends State { @override - Widget build(BuildContext context) => ListView.builder( - itemCount: widget.filteredUsers.length, - itemBuilder: (context, index) { - var user = widget.filteredUsers[index]; - var isSelected = widget.selectedUserList - .any((selectedUser) => selectedUser == user); - var theme = Theme.of(context); - return DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.colorScheme.secondary.withOpacity(0.3), - width: 0.5, - ), - ), - ), - child: InkWell( - onTap: () { - setState(() { - if (widget.selectedUserList.contains(user)) { - widget.selectedUserList.remove(user); - } else { - widget.selectedUserList.add(user); - } - }); - }, - child: Padding( - padding: widget.options.paddingAroundChatList ?? - const EdgeInsets.fromLTRB(28, 8, 28, 8), - child: ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, - horizontal: 30, - ), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: widget.options.userAvatarBuilder(user, 40.0), - ), - Expanded( - child: Container( - height: 40, - alignment: Alignment.centerLeft, - child: Text( - user.fullName ?? - widget.translations.anonymousUser, - style: theme.textTheme.bodyLarge, - ), - ), - ), - if (isSelected) ...[ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - Icons.check_circle, - color: theme.colorScheme.primary, - ), - ), - ], - ], - ), + void initState() { + widget.service.chatOverviewService.addListener(_listen); + super.initState(); + } + + @override + void dispose() { + widget.service.chatOverviewService.removeListener(_listen); + super.dispose(); + } + + void _listen() { + setState(() {}); + } + + @override + Widget build(BuildContext context) => Padding( + padding: widget.options.paddingAroundChatList ?? + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: ListView.builder( + itemCount: widget.filteredUsers.length, + itemBuilder: (context, index) { + var user = widget.filteredUsers[index]; + var isSelected = widget + .service.chatOverviewService.currentlySelectedUsers + .any((selectedUser) => selectedUser == user); + var theme = Theme.of(context); + return widget.options.chatRowContainerBuilder( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + widget.options.userAvatarBuilder(user, 44), + const SizedBox( + width: 12, + ), + Text( + user.fullName ?? widget.translations.anonymousUser, + style: theme.textTheme.titleMedium, + ), + ], ), - ), + Checkbox( + value: isSelected, + onChanged: (value) { + setState(() { + if (widget + .service.chatOverviewService.currentlySelectedUsers + .contains(user)) { + widget.service.chatOverviewService + .removeCurrentlySelectedUser(user); + } else { + widget.service.chatOverviewService + .addCurrentlySelectedUser(user); + } + }); + }, + ), + ], ), - ), - ); - }, + context, + ); + }, + ), ); } diff --git a/packages/flutter_chat_view/lib/src/services/date_formatter.dart b/packages/flutter_chat_view/lib/src/services/date_formatter.dart index 660b134..4d4f0c1 100644 --- a/packages/flutter_chat_view/lib/src/services/date_formatter.dart +++ b/packages/flutter_chat_view/lib/src/services/date_formatter.dart @@ -23,11 +23,39 @@ class DateFormatter { .inDays == 0; + bool _isYesterday(DateTime date) => + DateTime( + date.year, + date.month, + date.day, + ) + .difference( + DateTime( + _now.year, + _now.month, + _now.day, + ), + ) + .inDays == + -1; + + bool _isThisYear(DateTime date) => date.year == _now.year; + String format({ required DateTime date, bool showFullDate = false, - }) => - DateFormat( - _isToday(date) ? "HH:mm" : 'dd-MM-yyyy${showFullDate ? ' HH:mm' : ''}', - ).format(date); + }) { + if(showFullDate) { + return DateFormat("dd - MM - yyyy HH:mm").format(date); + } + if (_isToday(date)) { + return DateFormat("HH:mm").format(date); + } else if (_isYesterday(date)) { + return "yesterday"; + } else if (_isThisYear(date)) { + return DateFormat("dd MMMM").format(date); + } else { + return DateFormat("dd - MM - yyyy").format(date); + } + } } diff --git a/packages/flutter_chat_view/pubspec.yaml b/packages/flutter_chat_view/pubspec.yaml index 40ff8d3..90299f2 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: 3.0.1 +version: 3.1.0 publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub @@ -25,7 +25,7 @@ dependencies: version: ^1.0.5 flutter_profile: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^1.3.0 + version: ^1.5.0 dev_dependencies: flutter_test: