diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..906bbb3 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.24.3" +} \ No newline at end of file diff --git a/packages/chat_repository_interface/lib/src/models/chat_model.dart b/packages/chat_repository_interface/lib/src/models/chat_model.dart index b523498..f6cc8b9 100644 --- a/packages/chat_repository_interface/lib/src/models/chat_model.dart +++ b/packages/chat_repository_interface/lib/src/models/chat_model.dart @@ -25,6 +25,22 @@ class ChatModel { this.unreadMessageCount = 0, }); + /// The factory chat model that creates a chat model from a map + factory ChatModel.fromMap(String id, Map data) => ChatModel( + id: id, + users: List.from(data["users"]), + isGroupChat: data["isGroupChat"], + chatName: data["chatName"], + description: data["description"], + imageUrl: data["imageUrl"], + canBeDeleted: data["canBeDeleted"] ?? true, + lastUsed: data["lastUsed"] != null + ? DateTime.fromMillisecondsSinceEpoch(data["lastUsed"]) + : null, + lastMessage: data["lastMessage"], + unreadMessageCount: data["unreadMessageCount"] ?? 0, + ); + /// The chat id final String id; @@ -81,34 +97,17 @@ class ChatModel { unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount, ); - /// The factory chat model that creates a chat model from a map - factory ChatModel.fromMap(String id, Map data) { - return ChatModel( - id: id, - users: List.from(data['users']), - isGroupChat: data['isGroupChat'], - chatName: data['chatName'], - description: data['description'], - imageUrl: data['imageUrl'], - canBeDeleted: data['canBeDeleted'] ?? true, - lastUsed: data['lastUsed'] != null - ? DateTime.fromMillisecondsSinceEpoch(data['lastUsed']) - : null, - lastMessage: data['lastMessage'], - unreadMessageCount: data['unreadMessageCount'] ?? 0, - ); - } - + /// Creates a map representation of this object Map toMap() => { - 'users': users, - 'isGroupChat': isGroupChat, - 'chatName': chatName, - 'description': description, - 'imageUrl': imageUrl, - 'canBeDeleted': canBeDeleted, - 'lastUsed': lastUsed?.millisecondsSinceEpoch, - 'lastMessage': lastMessage, - 'unreadMessageCount': unreadMessageCount, + "users": users, + "isGroupChat": isGroupChat, + "chatName": chatName, + "description": description, + "imageUrl": imageUrl, + "canBeDeleted": canBeDeleted, + "lastUsed": lastUsed?.millisecondsSinceEpoch, + "lastMessage": lastMessage, + "unreadMessageCount": unreadMessageCount, }; } diff --git a/packages/chat_repository_interface/lib/src/models/message_model.dart b/packages/chat_repository_interface/lib/src/models/message_model.dart index 9b315d4..aee1b08 100644 --- a/packages/chat_repository_interface/lib/src/models/message_model.dart +++ b/packages/chat_repository_interface/lib/src/models/message_model.dart @@ -16,6 +16,17 @@ class MessageModel { required this.senderId, }); + /// Creates a message model instance given a map instance + factory MessageModel.fromMap(String id, Map map) => + MessageModel( + chatId: map["chatId"], + id: id, + text: map["text"], + imageUrl: map["imageUrl"], + timestamp: DateTime.fromMillisecondsSinceEpoch(map["timestamp"]), + senderId: map["senderId"], + ); + /// The chat id final String chatId; @@ -52,26 +63,14 @@ class MessageModel { senderId: senderId ?? this.senderId, ); - factory MessageModel.fromMap(String id, Map map) { - return MessageModel( - chatId: map['chatId'], - id: id, - text: map['text'], - imageUrl: map['imageUrl'], - timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp']), - senderId: map['senderId'], - ); - } - - Map toMap() { - return { - 'chatId': chatId, - 'text': text, - 'imageUrl': imageUrl, - 'timestamp': timestamp.millisecondsSinceEpoch, - 'senderId': senderId, - }; - } + /// Creates a map representation of this object + Map toMap() => { + "chatId": chatId, + "text": text, + "imageUrl": imageUrl, + "timestamp": timestamp.millisecondsSinceEpoch, + "senderId": senderId, + }; } /// Extension on [MessageModel] to check the message type diff --git a/packages/chat_repository_interface/lib/src/models/user_model.dart b/packages/chat_repository_interface/lib/src/models/user_model.dart index 3673615..4eb2645 100644 --- a/packages/chat_repository_interface/lib/src/models/user_model.dart +++ b/packages/chat_repository_interface/lib/src/models/user_model.dart @@ -14,6 +14,14 @@ class UserModel { this.imageUrl, }); + /// Creates a user based on a given map [data] + factory UserModel.fromMap(String id, Map data) => UserModel( + id: id, + firstName: data["first_name"], + lastName: data["last_name"], + imageUrl: data["image_url"], + ); + /// The user id final String id; @@ -25,15 +33,6 @@ class UserModel { /// The user image url final String? imageUrl; - - factory UserModel.fromMap(String id, Map data) { - return UserModel( - id: id, - firstName: data['firstName'], - lastName: data['lastName'], - imageUrl: data['imageUrl'], - ); - } } /// Extension on [UserModel] to get the user full name diff --git a/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart b/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart index ec26ad4..144fa75 100644 --- a/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart +++ b/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart @@ -1,24 +1,28 @@ -import 'dart:typed_data'; +import "dart:typed_data"; -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; +import "package:firebase_storage/firebase_storage.dart"; +/// Firebase implementation of the chat repository class FirebaseChatRepository implements ChatRepositoryInterface { - final FirebaseFirestore _firestore; - final FirebaseStorage _storage; - final String chatCollection; - final String messageCollection; - final String mediaPath; - + /// Creates a firebase implementation of the chat repository FirebaseChatRepository({ FirebaseFirestore? firestore, FirebaseStorage? storage, - this.chatCollection = 'chats', - this.messageCollection = 'messages', - this.mediaPath = 'chat', - }) : _firestore = firestore ?? FirebaseFirestore.instance, + String chatCollection = "chats", + String messageCollection = "messages", + String mediaPath = "chat", + }) : _mediaPath = mediaPath, + _messageCollection = messageCollection, + _chatCollection = chatCollection, + _firestore = firestore ?? FirebaseFirestore.instance, _storage = storage ?? FirebaseStorage.instance; + final FirebaseFirestore _firestore; + final FirebaseStorage _storage; + final String _chatCollection; + final String _messageCollection; + final String _mediaPath; @override Future createChat({ @@ -29,65 +33,62 @@ class FirebaseChatRepository implements ChatRepositoryInterface { String? imageUrl, List? messages, }) async { - final chatData = { - 'users': users, - 'isGroupChat': isGroupChat, - 'chatName': chatName, - 'description': description, - 'imageUrl': imageUrl, - 'createdAt': DateTime.now().millisecondsSinceEpoch, + var chatData = { + "users": users, + "isGroupChat": isGroupChat, + "chatName": chatName, + "description": description, + "imageUrl": imageUrl, + "createdAt": DateTime.now().millisecondsSinceEpoch, }; - await _firestore.collection(chatCollection).add(chatData); + await _firestore.collection(_chatCollection).add(chatData); } @override Future deleteChat({required String chatId}) async { - await _firestore.collection(chatCollection).doc(chatId).delete(); + await _firestore.collection(_chatCollection).doc(chatId).delete(); } @override - Stream getChat({required String chatId}) { - return _firestore - .collection(chatCollection) - .doc(chatId) - .snapshots() - .map((snapshot) { - var data = snapshot.data() as Map; - return ChatModel.fromMap(snapshot.id, data); - }); - } + Stream getChat({required String chatId}) => _firestore + .collection(_chatCollection) + .doc(chatId) + .snapshots() + .map((snapshot) { + var data = snapshot.data()!; + return ChatModel.fromMap(snapshot.id, data); + }); @override - Stream?> getChats({required String userId}) { - return _firestore - .collection(chatCollection) - .where('users', arrayContains: userId) - .snapshots() - .map((querySnapshot) { - return querySnapshot.docs.map((doc) { - var data = doc.data(); - return ChatModel.fromMap(doc.id, data); - }).toList(); - }); - } - - @override - Stream getMessage( - {required String chatId, required String messageId}) { - return _firestore - .collection(chatCollection) - .doc(chatId) - .collection(messageCollection) - .doc(messageId) - .snapshots() - .map((snapshot) { - var data = snapshot.data() as Map; - return MessageModel.fromMap( - snapshot.id, - data, + Stream?> getChats({required String userId}) => _firestore + .collection(_chatCollection) + .where("users", arrayContains: userId) + .snapshots() + .map( + (querySnapshot) => querySnapshot.docs.map((doc) { + var data = doc.data(); + return ChatModel.fromMap(doc.id, data); + }).toList(), ); - }); - } + + @override + Stream getMessage({ + required String chatId, + required String messageId, + }) => + _firestore + .collection(_chatCollection) + .doc(chatId) + .collection(_messageCollection) + .doc(messageId) + .snapshots() + .map((snapshot) { + var data = snapshot.data()!; + return MessageModel.fromMap( + snapshot.id, + data, + ); + }); @override Stream?> getMessages({ @@ -95,41 +96,46 @@ class FirebaseChatRepository implements ChatRepositoryInterface { required String userId, required int pageSize, required int page, - }) { - return _firestore - .collection(chatCollection) - .doc(chatId) - .collection(messageCollection) - .orderBy('timestamp') - .limit(pageSize) - .snapshots() - .map((query) => query.docs - .map((snapshot) => MessageModel.fromMap( - snapshot.id, - snapshot.data(), - )) - .toList()); - } + }) => + _firestore + .collection(_chatCollection) + .doc(chatId) + .collection(_messageCollection) + .orderBy("timestamp") + .limit(pageSize) + .snapshots() + .map( + (query) => query.docs + .map( + (snapshot) => MessageModel.fromMap( + snapshot.id, + snapshot.data(), + ), + ) + .toList(), + ); @override - Stream getUnreadMessagesCount( - {required String userId, String? chatId}) async* { + Stream getUnreadMessagesCount({ + required String userId, + String? chatId, + }) async* { var query = _firestore - .collection(chatCollection) - .where('users', arrayContains: userId) - .where('unreadMessageCount', isGreaterThan: 0) + .collection(_chatCollection) + .where("users", arrayContains: userId) + .where("unreadMessageCount", isGreaterThan: 0) .snapshots(); await for (var snapshot in query) { var count = 0; for (var doc in snapshot.docs) { var data = doc.data(); - var lastMessageKey = data['lastMessage']; + var lastMessageKey = data["lastMessage"]; var message = await getMessage(chatId: doc.id, messageId: lastMessageKey).first; if (message?.senderId != userId) { - count += data['unreadMessageCount'] as int; + count += data["unreadMessageCount"] as int; } } @@ -147,27 +153,28 @@ class FirebaseChatRepository implements ChatRepositoryInterface { DateTime? timestamp, }) async { var message = MessageModel( - chatId: chatId, - id: messageId, - text: text, - imageUrl: imageUrl, - timestamp: timestamp ?? DateTime.now(), - senderId: senderId); + chatId: chatId, + id: messageId, + text: text, + imageUrl: imageUrl, + timestamp: timestamp ?? DateTime.now(), + senderId: senderId, + ); await _firestore - .collection(chatCollection) + .collection(_chatCollection) .doc(chatId) - .collection(messageCollection) + .collection(_messageCollection) .doc(messageId) .set( message.toMap(), ); - await _firestore.collection(chatCollection).doc(chatId).update( + await _firestore.collection(_chatCollection).doc(chatId).update( { - 'lastMessage': messageId, - 'unreadMessageCount': FieldValue.increment(1), - 'lastUsed': DateTime.now().millisecondsSinceEpoch, + "lastMessage": messageId, + "unreadMessageCount": FieldValue.increment(1), + "lastUsed": DateTime.now().millisecondsSinceEpoch, }, ); } @@ -175,17 +182,19 @@ class FirebaseChatRepository implements ChatRepositoryInterface { @override Future updateChat({required ChatModel chat}) async { await _firestore - .collection(chatCollection) + .collection(_chatCollection) .doc(chat.id) .update(chat.toMap()); } @override - Future uploadImage( - {required String path, required Uint8List image}) async { - final ref = _storage.ref().child(mediaPath).child(path); - final uploadTask = ref.putData(image); - final snapshot = await uploadTask.whenComplete(() => {}); - return await snapshot.ref.getDownloadURL(); + Future uploadImage({ + required String path, + required Uint8List image, + }) async { + var ref = _storage.ref().child(_mediaPath).child(path); + var uploadTask = ref.putData(image); + var snapshot = await uploadTask.whenComplete(() => {}); + return snapshot.ref.getDownloadURL(); } } diff --git a/packages/firebase_chat_repository/lib/src/firebase_user_repository.dart b/packages/firebase_chat_repository/lib/src/firebase_user_repository.dart index 7a25aef..3426383 100644 --- a/packages/firebase_chat_repository/lib/src/firebase_user_repository.dart +++ b/packages/firebase_chat_repository/lib/src/firebase_user_repository.dart @@ -1,41 +1,36 @@ -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; +/// Firebase implementation of a user respository for chats. class FirebaseUserRepository implements UserRepositoryInterface { - final FirebaseFirestore _firestore; - final String userCollection; - + /// Creates a firebase implementation of a user respository for chats. FirebaseUserRepository({ FirebaseFirestore? firestore, - this.userCollection = 'users', - }) : _firestore = firestore ?? FirebaseFirestore.instance; + String userCollection = "users", + }) : _userCollection = userCollection, + _firestore = firestore ?? FirebaseFirestore.instance; + final FirebaseFirestore _firestore; + final String _userCollection; @override - Stream> getAllUsers() { - return _firestore - .collection(userCollection) - .snapshots() - .map((querySnapshot) { - return querySnapshot.docs - .map((doc) => UserModel.fromMap( - doc.id, - doc.data(), - )) - .toList(); - }); - } + Stream> getAllUsers() => + _firestore.collection(_userCollection).snapshots().map( + (querySnapshot) => querySnapshot.docs + .map( + (doc) => UserModel.fromMap( + doc.id, + doc.data(), + ), + ) + .toList(), + ); @override - Stream getUser({required String userId}) { - return _firestore - .collection(userCollection) - .doc(userId) - .snapshots() - .map((snapshot) { - return UserModel.fromMap( - snapshot.id, - snapshot.data() as Map, - ); - }); - } + Stream getUser({required String userId}) => + _firestore.collection(_userCollection).doc(userId).snapshots().map( + (snapshot) => UserModel.fromMap( + snapshot.id, + snapshot.data()!, + ), + ); } diff --git a/packages/flutter_chat/lib/src/config/screen_types.dart b/packages/flutter_chat/lib/src/config/screen_types.dart index 7ae873c..ffd334a 100644 --- a/packages/flutter_chat/lib/src/config/screen_types.dart +++ b/packages/flutter_chat/lib/src/config/screen_types.dart @@ -1,28 +1,41 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/screens/chat_detail_screen.dart'; -import 'package:flutter_chat/src/screens/chat_profile_screen.dart'; -import 'package:flutter_chat/src/screens/chat_screen.dart'; -import 'package:flutter_chat/src/screens/creation/new_chat_screen.dart'; -import 'package:flutter_chat/src/screens/creation/new_group_chat_overview.dart'; -import 'package:flutter_chat/src/screens/creation/new_group_chat_screen.dart'; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/screens/chat_detail_screen.dart"; +import "package:flutter_chat/src/screens/chat_profile_screen.dart"; +import "package:flutter_chat/src/screens/chat_screen.dart"; +import "package:flutter_chat/src/screens/creation/new_chat_screen.dart"; +import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart"; +import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart"; +/// Type of screen, used in custom screen builders enum ScreenType { + /// Screen displaying an overview of chats chatScreen(screen: ChatScreen), + + /// Screen displaying a single chat chatDetailScreen(screen: ChatDetailScreen), + + /// Screen displaying the profile of a user within a chat chatProfileScreen(screen: ChatProfileScreen), + + /// Screen with a form to create a new chat newChatScreen(screen: NewChatScreen), + + /// Screen with a form to create a new group chat newGroupChatScreen(screen: NewGroupChatScreen), + + /// Screen displaying all group chats newGroupChatOverview(screen: NewGroupChatOverview); const ScreenType({ - required this.screen, - }); + required Type screen, + }) : _screen = screen; - final Type screen; + final Type _screen; } +/// Extension for mapping widgets to [ScreenType]s extension MapFromWidget on Widget { - ScreenType get mapScreenType { - return ScreenType.values.firstWhere((e) => e.screen == this.runtimeType); - } + /// returns corresponding [ScreenType] + ScreenType get mapScreenType => + ScreenType.values.firstWhere((e) => e._screen == runtimeType); } diff --git a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart index 81ad20f..6c41a75 100644 --- a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart @@ -35,6 +35,7 @@ class ChatProfileScreen extends StatelessWidget { /// Callback function triggered when a user is tapped final Function(String)? onTapUser; + /// instance of a chat service final ChatService service; /// Callback function triggered when the start chat button is pressed @@ -135,6 +136,76 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { var theme = Theme.of(context); + + var chatUserDisplay = Wrap( + children: [ + ...chat!.users.map( + (tappedUser) => Padding( + padding: const EdgeInsets.only( + bottom: 8, + right: 8, + ), + child: InkWell( + onTap: () { + onTapUser?.call(tappedUser); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: service.getUser(userId: tappedUser).first, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } + + var user = snapshot.data; + + if (user == null) { + return const SizedBox.shrink(); + } + + return options.builders.userAvatarBuilder?.call( + context, + user, + 44, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: + user.imageUrl != null || user.imageUrl != "" + ? user.imageUrl + : null, + ), + size: 60, + ); + }, + ), + ], + ), + ), + ), + ), + ], + ); + + var targetUser = user ?? + ( + chat != null + ? UserModel( + id: UniqueKey().toString(), + firstName: chat?.chatName, + imageUrl: chat?.imageUrl, + ) + : UserModel( + id: UniqueKey().toString(), + firstName: options.translations.groupNameEmpty, + ), + ) as UserModel; return Stack( children: [ ListView( @@ -145,20 +216,7 @@ class _Body extends StatelessWidget { children: [ options.builders.userAvatarBuilder?.call( context, - user ?? - ( - chat != null - ? UserModel( - id: UniqueKey().toString(), - firstName: chat?.chatName, - imageUrl: chat?.imageUrl, - ) - : UserModel( - id: UniqueKey().toString(), - firstName: - options.translations.groupNameEmpty, - ), - ) as UserModel, + targetUser, 60, ) ?? Avatar( @@ -223,65 +281,7 @@ class _Body extends StatelessWidget { const SizedBox( height: 12, ), - Wrap( - children: [ - ...chat!.users.map( - (tappedUser) => Padding( - padding: const EdgeInsets.only( - bottom: 8, - right: 8, - ), - child: InkWell( - onTap: () { - onTapUser?.call(tappedUser); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FutureBuilder( - future: service - .getUser(userId: tappedUser) - .first, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return const CircularProgressIndicator(); - } - - var user = snapshot.data; - - if (user == null) { - return const SizedBox.shrink(); - } - - return options.builders.userAvatarBuilder - ?.call( - context, - user, - 44, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl != null || - user.imageUrl != "" - ? user.imageUrl - : null, - ), - size: 60, - ); - }, - ), - ], - ), - ), - ), - ), - ], - ), + chatUserDisplay, ], ), ),