From 1f3dc09f441e51b7f25ba8ece9d01163a7bc22d6 Mon Sep 17 00:00:00 2001 From: Niels Date: Fri, 9 Aug 2024 11:49:29 +0200 Subject: [PATCH] fix: feedback --- packages/chat_repository_interface/.gitignore | 2 +- packages/chat_repository_interface/LICENSE | 2 +- packages/chat_repository_interface/README.md | 39 -- .../analysis_options.yaml | 11 +- .../lib/chat_repository_interface.dart | 18 +- .../interfaces/chat_repostory_interface.dart | 65 ++- .../interfaces/user_repository_interface.dart | 9 +- .../lib/src/local/local_chat_repository.dart | 257 +++++----- .../lib/src/local/local_user_repository.dart | 52 +- .../lib/src/models/chat_model.dart | 91 ++-- .../lib/src/models/message_model.dart | 49 +- .../lib/src/models/user_model.dart | 19 +- .../lib/src/services/chat_service.dart | 205 +++++--- .../chat_repository_interface/pubspec.yaml | 42 +- packages/firebase_chat_repository/.gitignore | 2 +- packages/firebase_chat_repository/LICENSE | 2 +- .../analysis_options.yaml | 11 +- .../lib/firebase_chat_repository.dart | 2 - .../firebase_chat_repository/pubspec.yaml | 42 +- packages/flutter_chat/.gitignore | 2 +- packages/flutter_chat/analysis_options.yaml | 11 +- packages/flutter_chat/example/lib/main.dart | 47 +- packages/flutter_chat/example/pubspec.yaml | 76 +-- .../example/test/widget_test.dart | 30 ++ packages/flutter_chat/lib/flutter_chat.dart | 19 +- .../lib/src/config/chat_builders.dart | 48 +- .../lib/src/config/chat_options.dart | 49 +- .../lib/src/config/chat_translations.dart | 2 + .../lib/src/flutter_chat_entry_widget.dart | 4 +- .../src/flutter_chat_navigator_userstory.dart | 219 ++++++--- .../lib/src/screens/chat_detail_screen.dart | 454 ++++++++++-------- .../lib/src/screens/chat_profile_screen.dart | 106 ++-- .../lib/src/screens/chat_screen.dart | 237 +++++---- .../src/screens/creation/new_chat_screen.dart | 29 +- .../creation/new_group_chat_overview.dart | 127 ++--- .../creation/new_group_chat_screen.dart | 27 +- .../creation/widgets/image_picker.dart | 17 +- .../creation/widgets/search_field.dart | 56 ++- .../screens/creation/widgets/search_icon.dart | 9 +- .../screens/creation/widgets/user_list.dart | 34 +- .../lib/src/services/date_formatter.dart | 5 + packages/flutter_chat/pubspec.yaml | 53 +- 42 files changed, 1509 insertions(+), 1072 deletions(-) mode change 100644 => 120000 packages/chat_repository_interface/LICENSE delete mode 100644 packages/chat_repository_interface/README.md mode change 100644 => 120000 packages/firebase_chat_repository/LICENSE create mode 100644 packages/flutter_chat/example/test/widget_test.dart diff --git a/packages/chat_repository_interface/.gitignore b/packages/chat_repository_interface/.gitignore index ac5aa98..49a187c 100644 --- a/packages/chat_repository_interface/.gitignore +++ b/packages/chat_repository_interface/.gitignore @@ -19,7 +19,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. diff --git a/packages/chat_repository_interface/LICENSE b/packages/chat_repository_interface/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/packages/chat_repository_interface/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/packages/chat_repository_interface/LICENSE b/packages/chat_repository_interface/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/chat_repository_interface/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/chat_repository_interface/README.md b/packages/chat_repository_interface/README.md deleted file mode 100644 index 02fe8ec..0000000 --- a/packages/chat_repository_interface/README.md +++ /dev/null @@ -1,39 +0,0 @@ - - -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. - -## Features - -TODO: List what your package can do. Maybe include images, gifs, or videos. - -## Getting started - -TODO: List prerequisites and provide or point to information on how to -start using the package. - -## Usage - -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. - -```dart -const like = 'sample'; -``` - -## Additional information - -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. diff --git a/packages/chat_repository_interface/analysis_options.yaml b/packages/chat_repository_interface/analysis_options.yaml index a5744c1..bbf71fe 100644 --- a/packages/chat_repository_interface/analysis_options.yaml +++ b/packages/chat_repository_interface/analysis_options.yaml @@ -1,4 +1,9 @@ -include: package:flutter_lints/flutter.yaml +include: package:flutter_iconica_analysis/components_options.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: \ No newline at end of file diff --git a/packages/chat_repository_interface/lib/chat_repository_interface.dart b/packages/chat_repository_interface/lib/chat_repository_interface.dart index 5fa1623..c8b2805 100644 --- a/packages/chat_repository_interface/lib/chat_repository_interface.dart +++ b/packages/chat_repository_interface/lib/chat_repository_interface.dart @@ -1,17 +1,15 @@ -library chat_repository_interface; - // Interfaces -export 'src/interfaces/chat_repostory_interface.dart'; -export 'src/interfaces/user_repository_interface.dart'; +export "src/interfaces/chat_repostory_interface.dart"; +export "src/interfaces/user_repository_interface.dart"; // Local implementations -export 'src/local/local_chat_repository.dart'; -export 'src/local/local_user_repository.dart'; +export "src/local/local_chat_repository.dart"; +export "src/local/local_user_repository.dart"; // Models -export 'src/models/chat_model.dart'; -export 'src/models/message_model.dart'; -export 'src/models/user_model.dart'; +export "src/models/chat_model.dart"; +export "src/models/message_model.dart"; +export "src/models/user_model.dart"; // Services -export 'src/services/chat_service.dart'; +export "src/services/chat_service.dart"; diff --git a/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart b/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart index ebb4202..b1a23b5 100644 --- a/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart +++ b/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart @@ -1,30 +1,53 @@ -import 'dart:typed_data'; +import "dart:typed_data"; -import 'package:chat_repository_interface/src/models/chat_model.dart'; -import 'package:chat_repository_interface/src/models/message_model.dart'; -import 'package:chat_repository_interface/src/models/user_model.dart'; +import "package:chat_repository_interface/src/models/chat_model.dart"; +import "package:chat_repository_interface/src/models/message_model.dart"; +import "package:chat_repository_interface/src/models/user_model.dart"; +/// The chat repository interface +/// Implement this interface to create a chat +/// repository with a given data source. abstract class ChatRepositoryInterface { - String createChat({ - required List users, + /// Create a chat with the given parameters. + /// [users] is a list of [UserModel] that will be part of the chat. + /// [chatName] is the name of the chat. + /// [description] is the description of the chat. + /// [imageUrl] is the image url of the chat. + /// [messages] is a list of [MessageModel] that will be part of the chat. + Future createChat({ + required List users, + required bool isGroupChat, String? chatName, String? description, String? imageUrl, List? messages, }); - Stream updateChat({ + /// Update the chat with the given parameters. + /// [chat] is the chat that will be updated. + Future updateChat({ required ChatModel chat, }); + /// Get the chat with the given [chatId]. + /// Returns a [ChatModel] stream. Stream getChat({ required String chatId, }); + /// Get the chats for the given [userId]. + /// Returns a list of [ChatModel] stream. Stream?> getChats({ required String userId, }); + /// Get the messages for the given [chatId]. + /// Returns a list of [MessageModel] stream. + /// [pageSize] is the number of messages to be fetched. + /// [page] is the page number. + /// [userId] is the user id. + /// [chatId] is the chat id. + /// Returns a list of [MessageModel] stream. Stream?> getMessages({ required String chatId, required String userId, @@ -32,22 +55,46 @@ abstract class ChatRepositoryInterface { required int page, }); - bool sendMessage({ + /// Get the message with the given [messageId]. + /// [chatId] is the chat id. + /// Returns a [MessageModel] stream. + Stream getMessage({ + required String chatId, + required String messageId, + }); + + /// Send a message with the given parameters. + /// [chatId] is the chat id. + /// [senderId] is the sender id. + /// [text] is the message text. + /// [imageUrl] is the image url. + Future sendMessage({ required String chatId, required String senderId, + required String messageId, String? text, String? imageUrl, + DateTime? timestamp, }); - bool deleteChat({ + /// Delete the chat with the given [chatId]. + Future deleteChat({ required String chatId, }); + /// Get the unread messages count for the given [userId]. + /// [chatId] is the chat id. If not provided, it will return the + /// total unread messages count. + /// Returns an integer stream. Stream getUnreadMessagesCount({ required String userId, String? chatId, }); + /// Upload an image with the given parameters. + /// [path] is the path of the image. + /// [image] is the image data. + /// Returns the image url. Future uploadImage({ required String path, required Uint8List image, diff --git a/packages/chat_repository_interface/lib/src/interfaces/user_repository_interface.dart b/packages/chat_repository_interface/lib/src/interfaces/user_repository_interface.dart index b18912f..323d2d0 100644 --- a/packages/chat_repository_interface/lib/src/interfaces/user_repository_interface.dart +++ b/packages/chat_repository_interface/lib/src/interfaces/user_repository_interface.dart @@ -1,7 +1,14 @@ -import 'package:chat_repository_interface/src/models/user_model.dart'; +import "package:chat_repository_interface/src/models/user_model.dart"; +/// The user repository interface +/// Implement this interface to create a user +/// repository with a given data source. abstract class UserRepositoryInterface { + /// Get the user with the given [userId]. + /// Returns a [UserModel] stream. Stream getUser({required String userId}); + /// Get all the users. + /// Returns a list of [UserModel] stream. Stream> getAllUsers(); } diff --git a/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart b/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart index a8faac7..7ecb6f3 100644 --- a/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart +++ b/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart @@ -1,114 +1,110 @@ -import 'dart:async'; -import 'dart:math'; -import 'dart:typed_data'; +import "dart:async"; +import "dart:typed_data"; -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:collection/collection.dart'; -import 'package:rxdart/rxdart.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:collection/collection.dart"; +import "package:rxdart/rxdart.dart"; +/// The local chat repository class LocalChatRepository implements ChatRepositoryInterface { - LocalChatRepository() { - var messages = []; + /// The local chat repository constructor + LocalChatRepository(); - for (var i = 0; i < 50; i++) { - var rnd = Random().nextInt(2); - - messages.add(MessageModel( - id: i.toString(), - text: 'Message $i', - senderId: rnd == 0 ? '1' : '2', - timestamp: DateTime.now().add(Duration(seconds: i)), - imageUrl: null, - )); - } - - _chats = [ - ChatModel( - id: '1', - users: [UserModel(id: '1'), UserModel(id: '2')], - messages: messages, - lastMessage: messages.last, - unreadMessageCount: 50, - ), - ]; - } - - StreamController> chatsController = + final StreamController> _chatsController = BehaviorSubject>(); - StreamController chatController = BehaviorSubject(); + final StreamController _chatController = + BehaviorSubject(); - StreamController> messageController = + final StreamController> _messageController = BehaviorSubject>(); - List _chats = []; + final List _chats = []; + final Map> _messages = {}; @override - String createChat( - {required List users, - String? chatName, - String? description, - String? imageUrl, - List? messages}) { + Future createChat({ + required List users, + required bool isGroupChat, + String? chatName, + String? description, + String? imageUrl, + List? messages, + }) async { var chat = ChatModel( id: DateTime.now().toString(), + isGroupChat: isGroupChat, users: users, - messages: messages ?? [], chatName: chatName, description: description, imageUrl: imageUrl, ); _chats.add(chat); - chatsController.add(_chats); + _chatsController.add(_chats); - return chat.id; + if (messages != null) { + for (var message in messages) { + await sendMessage( + messageId: message.id, + chatId: chat.id, + senderId: message.senderId, + text: message.text, + imageUrl: message.imageUrl, + timestamp: message.timestamp, + ); + } + } } @override - Stream updateChat({required ChatModel chat}) { + Future updateChat({ + required ChatModel chat, + }) async { var index = _chats.indexWhere((e) => e.id == chat.id); if (index != -1) { _chats[index] = chat; - chatsController.add(_chats); + _chatsController.add(_chats); } - - return chatController.stream.where((e) => e.id == chat.id); } @override - bool deleteChat({required String chatId}) { + Future deleteChat({ + required String chatId, + }) async { try { _chats.removeWhere((e) => e.id == chatId); - chatsController.add(_chats); - - return true; - } catch (e) { - return false; + _chatsController.add(_chats); + } on Exception catch (_) { + rethrow; } } @override - Stream getChat({required String chatId}) { + Stream getChat({ + required String chatId, + }) { var chat = _chats.firstWhereOrNull((e) => e.id == chatId); if (chat != null) { - chatController.add(chat); + _chatController.add(chat); - if (chat.imageUrl != null && chat.imageUrl!.isNotEmpty) { - chat.copyWith(imageUrl: 'https://picsum.photos/200/300'); + if (chat.imageUrl?.isNotEmpty ?? false) { + chat.copyWith(imageUrl: "https://picsum.photos/200/300"); } } - return chatController.stream; + return _chatController.stream; } @override - Stream?> getChats({required String userId}) { - chatsController.add(_chats); + Stream?> getChats({ + required String userId, + }) { + _chatsController.add(_chats); - return chatsController.stream; + return _chatsController.stream; } @override @@ -123,57 +119,75 @@ class LocalChatRepository implements ChatRepositoryInterface { chat = _chats.firstWhereOrNull((e) => e.id == chatId); if (chat != null) { - var messages = List.from(chat.messages); + var messages = List.from(_messages[chatId] ?? []); messages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - messageController.stream.first - .timeout( - const Duration(seconds: 1), - ) - .then((oldMessages) { - var newMessages = messages.reversed - .skip(page * pageSize) - .take(pageSize) - .toList(growable: false) - .reversed - .toList(); + unawaited( + _messageController.stream.first + .timeout( + const Duration(seconds: 1), + ) + .then((oldMessages) { + var newMessages = messages.reversed + .skip(page * pageSize) + .take(pageSize) + .toList(growable: false) + .reversed + .toList(); - if (newMessages.isEmpty) return; + if (newMessages.isEmpty) return; - var allMessages = [...oldMessages, ...newMessages]; + var allMessages = [...oldMessages, ...newMessages]; - allMessages = allMessages - .toSet() - .toList() - .cast() - .toList(growable: false); + allMessages = allMessages + .toSet() + .toList() + .cast() + .toList(growable: false); - allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - messageController.add(allMessages); - }).onError((error, stackTrace) { - messageController.add(messages.reversed - .skip(page * pageSize) - .take(pageSize) - .toList(growable: false) - .reversed - .toList()); - }); + _messageController.add(allMessages); + }).onError((error, stackTrace) { + _messageController.add( + messages.reversed + .skip(page * pageSize) + .take(pageSize) + .toList(growable: false) + .reversed + .toList(), + ); + }), + ); } - return messageController.stream; + return _messageController.stream; } @override - bool sendMessage( - {required String chatId, - required String senderId, - String? text, - String? imageUrl}) { + Stream getMessage({ + required String chatId, + required String messageId, + }) { + var message = _messages[chatId]?.firstWhereOrNull((e) => e.id == messageId); + + return Stream.value(message); + } + + @override + Future sendMessage({ + required String chatId, + required String senderId, + required String messageId, + String? text, + String? imageUrl, + DateTime? timestamp, + }) async { var message = MessageModel( - id: DateTime.now().toString(), - timestamp: DateTime.now(), + chatId: chatId, + id: messageId, + timestamp: timestamp ?? DateTime.now(), text: text, senderId: senderId, imageUrl: imageUrl, @@ -181,34 +195,45 @@ class LocalChatRepository implements ChatRepositoryInterface { var chat = _chats.firstWhereOrNull((e) => e.id == chatId); - if (chat == null) return false; + if (chat == null) throw Exception("Chat not found"); - chat.messages.add(message); - messageController.add(chat.messages); + var messages = List.from(_messages[chatId] ?? []); + messages.add(message); + _messages[chatId] = messages; - return true; + var newChat = chat.copyWith( + lastMessage: messageId, + unreadMessageCount: chat.unreadMessageCount + 1, + lastUsed: DateTime.now(), + ); + + _chats[_chats.indexWhere((e) => e.id == chatId)] = newChat; + + _chatsController.add(_chats); + _messageController.add(_messages[chatId] ?? []); } @override - Stream getUnreadMessagesCount({required String userId, String? chatId}) { - return chatsController.stream.map((chats) { - var count = 0; + Stream getUnreadMessagesCount({ + required String userId, + String? chatId, + }) => + _chatsController.stream.map((chats) { + var count = 0; - for (var chat in chats) { - if (chat.users.any((e) => e.id == userId)) { - count += chat.unreadMessageCount; + for (var chat in chats) { + if (chat.users.contains(userId)) { + count += chat.unreadMessageCount; + } } - } - return count; - }); - } + return count; + }); @override Future uploadImage({ required String path, required Uint8List image, - }) { - return Future.value('https://picsum.photos/200/300'); - } + }) => + Future.value("https://picsum.photos/200/300"); } diff --git a/packages/chat_repository_interface/lib/src/local/local_user_repository.dart b/packages/chat_repository_interface/lib/src/local/local_user_repository.dart index 0965bb8..c57ef7f 100644 --- a/packages/chat_repository_interface/lib/src/local/local_user_repository.dart +++ b/packages/chat_repository_interface/lib/src/local/local_user_repository.dart @@ -1,47 +1,51 @@ -import 'dart:async'; +import "dart:async"; -import 'package:chat_repository_interface/src/interfaces/user_repository_interface.dart'; -import 'package:chat_repository_interface/src/models/user_model.dart'; -import 'package:rxdart/rxdart.dart'; +import "package:chat_repository_interface/src/interfaces/user_repository_interface.dart"; +import "package:chat_repository_interface/src/models/user_model.dart"; +import "package:rxdart/rxdart.dart"; +/// The local user repository class LocalUserRepository implements UserRepositoryInterface { final StreamController> _usersController = BehaviorSubject>(); final List _users = [ UserModel( - id: '1', - firstName: 'John', - lastName: 'Doe', - imageUrl: 'https://picsum.photos/200/300', + id: "1", + firstName: "John", + lastName: "Doe", + imageUrl: "https://picsum.photos/200/300", ), UserModel( - id: '2', - firstName: 'Jane', - lastName: 'Doe', - imageUrl: 'https://picsum.photos/200/300', + id: "2", + firstName: "Jane", + lastName: "Doe", + imageUrl: "https://picsum.photos/200/300", ), UserModel( - id: '3', - firstName: 'Frans', - lastName: 'Timmermans', - imageUrl: 'https://picsum.photos/200/300', + id: "3", + firstName: "Frans", + lastName: "Timmermans", + imageUrl: "https://picsum.photos/200/300", ), UserModel( - id: '4', - firstName: 'Hendrik-Jan', - lastName: 'De derde', - imageUrl: 'https://picsum.photos/200/300', + id: "4", + firstName: "Hendrik-Jan", + lastName: "De derde", + imageUrl: "https://picsum.photos/200/300", ), ]; @override - Stream getUser({required String userId}) { - return getAllUsers().map((users) => users.firstWhere( + Stream getUser({ + required String userId, + }) => + getAllUsers().map( + (users) => users.firstWhere( (e) => e.id == userId, orElse: () => throw Exception(), - )); - } + ), + ); @override Stream> getAllUsers() { 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 d4d5655..3997414 100644 --- a/packages/chat_repository_interface/lib/src/models/chat_model.dart +++ b/packages/chat_repository_interface/lib/src/models/chat_model.dart @@ -1,11 +1,21 @@ -import 'package:chat_repository_interface/src/models/message_model.dart'; -import 'package:chat_repository_interface/src/models/user_model.dart'; - +/// The chat model +/// A model that represents a chat. +/// [id] is the chat id. +/// [users] is a list of [UserModel] that are part of the chat. +/// [chatName] is the name of the chat. +/// [description] is the description of the chat. +/// [imageUrl] is the image url of the chat. +/// [canBeDeleted] is a boolean that indicates if the chat can be deleted. +/// [lastUsed] is the last time the chat was used. +/// [lastMessage] is the last message of the chat. +/// [unreadMessageCount] is the number of unread messages in the chat. +/// Returns a [ChatModel] instance. class ChatModel { - ChatModel({ + /// The chat model constructor + const ChatModel({ required this.id, required this.users, - required this.messages, + required this.isGroupChat, this.chatName, this.description, this.imageUrl, @@ -15,51 +25,68 @@ class ChatModel { this.unreadMessageCount = 0, }); + /// The chat id final String id; - final List messages; - final List users; + + /// The chat users + final List users; + + /// The chat name final String? chatName; + + /// The chat description final String? description; + + /// The chat image url final String? imageUrl; + /// A boolean that indicates if the chat can be deleted final bool canBeDeleted; + + /// The last time the chat was used final DateTime? lastUsed; - final MessageModel? lastMessage; + + /// The last message of the chat + final String? lastMessage; + + /// The number of unread messages in the chat final int unreadMessageCount; + /// A boolean that indicates if the chat is a group chat + final bool isGroupChat; + + /// The chat model copy with method ChatModel copyWith({ String? id, - List? messages, - List? users, + List? users, String? chatName, String? description, String? imageUrl, bool? canBeDeleted, DateTime? lastUsed, - MessageModel? lastMessage, + String? lastMessage, int? unreadMessageCount, - }) { - return ChatModel( - id: id ?? this.id, - messages: messages ?? this.messages, - users: users ?? this.users, - chatName: chatName ?? this.chatName, - description: description ?? this.description, - imageUrl: imageUrl ?? this.imageUrl, - canBeDeleted: canBeDeleted ?? this.canBeDeleted, - lastUsed: lastUsed ?? this.lastUsed, - lastMessage: lastMessage ?? this.lastMessage, - unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount, - ); - } -} - -extension IsGroupChat on ChatModel { - bool get isGroupChat => users.length > 2; + bool? isGroupChat, + }) => + ChatModel( + id: id ?? this.id, + users: users ?? this.users, + chatName: chatName ?? this.chatName, + isGroupChat: isGroupChat ?? this.isGroupChat, + description: description ?? this.description, + imageUrl: imageUrl ?? this.imageUrl, + canBeDeleted: canBeDeleted ?? this.canBeDeleted, + lastUsed: lastUsed ?? this.lastUsed, + lastMessage: lastMessage ?? this.lastMessage, + unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount, + ); } +/// The chat model extension +/// An extension that adds extra functionality to the chat model. +/// [getOtherUser] is a method that returns the other user in the chat. extension GetOtherUser on ChatModel { - UserModel getOtherUser(String userId) { - return users.firstWhere((user) => user.id != userId); - } + /// The get other user method + String getOtherUser(String userId) => + users.firstWhere((user) => user != userId); } 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 c839ace..b2c8c8f 100644 --- a/packages/chat_repository_interface/lib/src/models/message_model.dart +++ b/packages/chat_repository_interface/lib/src/models/message_model.dart @@ -1,5 +1,14 @@ +/// Message model +/// Represents a message in a chat +/// [id] is the message id. +/// [text] is the message text. +/// [imageUrl] is the message image url. +/// [timestamp] is the message timestamp. +/// [senderId] is the sender id. class MessageModel { - MessageModel({ + /// Message model constructor + const MessageModel({ + required this.chatId, required this.id, required this.text, required this.imageUrl, @@ -7,31 +16,47 @@ class MessageModel { required this.senderId, }); + final String chatId; + + /// The message id final String id; + + /// The message text final String? text; + + /// The message image url final String? imageUrl; + + /// The message timestamp final DateTime timestamp; + + /// The sender id final String senderId; + /// The message model copy with method MessageModel copyWith({ + String? chatId, String? id, String? text, String? imageUrl, DateTime? timestamp, String? senderId, - }) { - return MessageModel( - id: id ?? this.id, - text: text ?? this.text, - imageUrl: imageUrl ?? this.imageUrl, - timestamp: timestamp ?? this.timestamp, - senderId: senderId ?? this.senderId, - ); - } + }) => + MessageModel( + chatId: chatId ?? this.chatId, + id: id ?? this.id, + text: text ?? this.text, + imageUrl: imageUrl ?? this.imageUrl, + timestamp: timestamp ?? this.timestamp, + senderId: senderId ?? this.senderId, + ); } +/// Extension on [MessageModel] to check the message type extension MessageType on MessageModel { - bool isTextMessage() => text != null; + /// Check if the message is a text message + bool get isTextMessage => text != null; - bool isImageMessage() => imageUrl != null; + /// Check if the message is an image message + bool get isImageMessage => imageUrl != null; } 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 9d2d576..87fbe2c 100644 --- a/packages/chat_repository_interface/lib/src/models/user_model.dart +++ b/packages/chat_repository_interface/lib/src/models/user_model.dart @@ -1,18 +1,35 @@ +/// User model +/// Represents a user in a chat +/// [id] is the user id. +/// [firstName] is the user first name. +/// [lastName] is the user last name. +/// [imageUrl] is the user image url. +/// [fullname] is the user full name. class UserModel { - UserModel({ + /// User model constructor + const UserModel({ required this.id, this.firstName, this.lastName, this.imageUrl, }); + /// The user id final String id; + + /// The user first name final String? firstName; + + /// The user last name final String? lastName; + + /// The user image url final String? imageUrl; } +/// Extension on [UserModel] to get the user full name extension Fullname on UserModel { + /// Get the user full name String? get fullname { if (firstName == null && lastName == null) { return null; diff --git a/packages/chat_repository_interface/lib/src/services/chat_service.dart b/packages/chat_repository_interface/lib/src/services/chat_service.dart index e95e181..2f9c07e 100644 --- a/packages/chat_repository_interface/lib/src/services/chat_service.dart +++ b/packages/chat_repository_interface/lib/src/services/chat_service.dart @@ -1,142 +1,191 @@ -import 'dart:async'; -import 'dart:typed_data'; +import "dart:async"; +import "dart:typed_data"; -import 'package:chat_repository_interface/src/interfaces/chat_repostory_interface.dart'; -import 'package:chat_repository_interface/src/interfaces/user_repository_interface.dart'; -import 'package:chat_repository_interface/src/local/local_chat_repository.dart'; -import 'package:chat_repository_interface/src/local/local_user_repository.dart'; -import 'package:chat_repository_interface/src/models/chat_model.dart'; -import 'package:chat_repository_interface/src/models/message_model.dart'; -import 'package:chat_repository_interface/src/models/user_model.dart'; -import 'package:collection/collection.dart'; +import "package:chat_repository_interface/src/interfaces/chat_repostory_interface.dart"; +import "package:chat_repository_interface/src/interfaces/user_repository_interface.dart"; +import "package:chat_repository_interface/src/local/local_chat_repository.dart"; +import "package:chat_repository_interface/src/local/local_user_repository.dart"; +import "package:chat_repository_interface/src/models/chat_model.dart"; +import "package:chat_repository_interface/src/models/message_model.dart"; +import "package:chat_repository_interface/src/models/user_model.dart"; +import "package:collection/collection.dart"; +/// The chat service +/// Use this service to interact with the chat repository. +/// Optionally provide a [chatRepository] and [userRepository] class ChatService { - final ChatRepositoryInterface chatRepository; - final UserRepositoryInterface userRepository; - + /// Create a chat service with the given parameters. ChatService({ ChatRepositoryInterface? chatRepository, UserRepositoryInterface? userRepository, }) : chatRepository = chatRepository ?? LocalChatRepository(), userRepository = userRepository ?? LocalUserRepository(); - Stream createChat({ + /// The chat repository + final ChatRepositoryInterface chatRepository; + + /// The user repository + final UserRepositoryInterface userRepository; + + /// Create a chat with the given parameters. + /// [users] is a list of [UserModel] that will be part of the chat. + /// [chatName] is the name of the chat. + /// [description] is the description of the chat. + /// [imageUrl] is the image url of the chat. + /// [messages] is a list of [MessageModel] that will be part of the chat. + /// Returns a [ChatModel] stream. + Future createChat({ required List users, + required bool isGroupChat, String? chatName, String? description, String? imageUrl, List? messages, }) { - var chatId = chatRepository.createChat( - users: users, + var userIds = users.map((e) => e.id).toList(); + + return chatRepository.createChat( + isGroupChat: isGroupChat, + users: userIds, chatName: chatName, description: description, imageUrl: imageUrl, messages: messages, ); - - return chatRepository.getChat(chatId: chatId); } + /// Get the chats for the given [userId]. + /// Returns a list of [ChatModel] stream. Stream?> getChats({ required String userId, - }) { - return chatRepository.getChats(userId: userId); - } + }) => + chatRepository.getChats(userId: userId); + /// Get the chat with the given [chatId]. + /// Returns a [ChatModel] stream. Stream getChat({ required String chatId, - }) { - return chatRepository.getChat(chatId: chatId); - } + }) => + chatRepository.getChat(chatId: chatId); + /// Get the chat with the given [currentUser] and [otherUser]. + /// Returns a [ChatModel] stream. + /// Returns null if the chat does not exist. Future getChatByUser({ required String currentUser, required String otherUser, }) async { - var chats = await chatRepository - .getChats(userId: currentUser) - .first - .timeout(const Duration(seconds: 1)); + var chats = await chatRepository.getChats(userId: currentUser).first; var personalChats = chats?.where((element) => element.users.length == 2).toList(); return personalChats?.firstWhereOrNull( - (element) => element.users.where((e) => e.id == otherUser).isNotEmpty, + (element) => element.users.where((e) => e == otherUser).isNotEmpty, ); } + /// Get the group chats with the given [currentUser] and [otherUsers]. + /// Returns a [ChatModel] stream. Future getGroupChatByUser({ required String currentUser, required List otherUsers, required String chatName, required String description, }) async { - var chats = await chatRepository - .getChats(userId: currentUser) - .first - .timeout(const Duration(seconds: 1)); - - var personalChats = - chats?.where((element) => element.users.length > 2).toList(); - try { + var chats = await chatRepository.getChats(userId: currentUser).first; + + var personalChats = + chats?.where((element) => element.isGroupChat).toList(); + var groupChats = personalChats - ?.where((chats) => otherUsers.every(chats.users.contains)) + ?.where( + (chats) => + otherUsers.every((user) => chats.users.contains(user.id)), + ) .toList(); return groupChats?.firstWhereOrNull( (element) => element.chatName == chatName && element.description == description, ); - } catch (e) { - return null; + // ignore: avoid_catches_without_on_clauses + } catch (_) { + throw Exception("Chat not found"); } } + /// Get the message with the given [messageId]. + /// [chatId] is the chat id. + /// Returns a [MessageModel] stream. + Stream getMessage({ + required String chatId, + required String messageId, + }) => + chatRepository.getMessage(chatId: chatId, messageId: messageId); + + /// Get the messages for the given [chatId]. + /// Returns a list of [MessageModel] stream. + /// [pageSize] is the number of messages to be fetched. + /// [page] is the page number. + /// [userId] is the user id. + /// [chatId] is the chat id. + /// Returns a list of [MessageModel] stream. Stream?> getMessages({ required String userId, required String chatId, required int pageSize, required int page, - }) { - return chatRepository.getMessages( - userId: userId, - chatId: chatId, - pageSize: pageSize, - page: page, - ); - } + }) => + chatRepository.getMessages( + userId: userId, + chatId: chatId, + pageSize: pageSize, + page: page, + ); - bool sendMessage({ + /// Send a message with the given parameters. + /// [chatId] is the chat id. + /// [senderId] is the sender id. + /// [text] is the message text. + /// [imageUrl] is the image url. + Future sendMessage({ required String chatId, - String? text, required String senderId, + required String messageId, + String? text, String? imageUrl, - }) { - return chatRepository.sendMessage( - chatId: chatId, - text: text, - senderId: senderId, - imageUrl: imageUrl, - ); - } + }) => + chatRepository.sendMessage( + chatId: chatId, + messageId: messageId, + text: text, + senderId: senderId, + imageUrl: imageUrl, + ); - bool deleteChat({ + /// Delete the chat with the given parameters. + /// [chatId] is the chat id. + Future deleteChat({ required String chatId, - }) { - return chatRepository.deleteChat(chatId: chatId); - } + }) => + chatRepository.deleteChat(chatId: chatId); - Stream getUser({required String userId}) { - return userRepository.getUser(userId: userId); - } + /// Get user with the given [userId]. + /// Returns a [UserModel] stream. + Stream getUser({required String userId}) => + userRepository.getUser(userId: userId); - Stream> getAllUsers() { - return userRepository.getAllUsers(); - } + /// Get all the users. + /// Returns a list of [UserModel] stream. + Stream> getAllUsers() => userRepository.getAllUsers(); + /// Get the unread messages count for the given [userId] and or [chatId]. + /// [userId] is the user id. + /// [chatId] is the chat id. If not provided, it will return the + /// total unread messages count. + /// Returns a [Stream] of [int]. Stream getUnreadMessagesCount({ required String userId, String? chatId, @@ -151,16 +200,22 @@ class ChatService { ); } + /// Upload an image with the given parameters. + /// [path] is the image path. + /// [image] is the image bytes. + /// Returns a [Future] of [String]. Future uploadImage({ required String path, required Uint8List image, - }) { - return chatRepository.uploadImage( - path: path, - image: image, - ); - } + }) => + chatRepository.uploadImage( + path: path, + image: image, + ); + /// Mark the chat as read with the given parameters. + /// [chatId] is the chat id. + /// Returns a [Future] of [void]. Future markAsRead({ required String chatId, }) async { @@ -171,6 +226,6 @@ class ChatService { unreadMessageCount: 0, ); - chatRepository.updateChat(chat: newChat); + await chatRepository.updateChat(chat: newChat); } } diff --git a/packages/chat_repository_interface/pubspec.yaml b/packages/chat_repository_interface/pubspec.yaml index b9d789e..1c2fed2 100644 --- a/packages/chat_repository_interface/pubspec.yaml +++ b/packages/chat_repository_interface/pubspec.yaml @@ -17,41 +17,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages +flutter: \ No newline at end of file diff --git a/packages/firebase_chat_repository/.gitignore b/packages/firebase_chat_repository/.gitignore index ac5aa98..49a187c 100644 --- a/packages/firebase_chat_repository/.gitignore +++ b/packages/firebase_chat_repository/.gitignore @@ -19,7 +19,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. diff --git a/packages/firebase_chat_repository/LICENSE b/packages/firebase_chat_repository/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/packages/firebase_chat_repository/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/packages/firebase_chat_repository/LICENSE b/packages/firebase_chat_repository/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/firebase_chat_repository/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/firebase_chat_repository/analysis_options.yaml b/packages/firebase_chat_repository/analysis_options.yaml index a5744c1..bbf71fe 100644 --- a/packages/firebase_chat_repository/analysis_options.yaml +++ b/packages/firebase_chat_repository/analysis_options.yaml @@ -1,4 +1,9 @@ -include: package:flutter_lints/flutter.yaml +include: package:flutter_iconica_analysis/components_options.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: \ No newline at end of file diff --git a/packages/firebase_chat_repository/lib/firebase_chat_repository.dart b/packages/firebase_chat_repository/lib/firebase_chat_repository.dart index 4ca7268..298576d 100644 --- a/packages/firebase_chat_repository/lib/firebase_chat_repository.dart +++ b/packages/firebase_chat_repository/lib/firebase_chat_repository.dart @@ -1,5 +1,3 @@ -library firebase_chat_repository; - /// A Calculator. class Calculator { /// Returns [value] plus 1. diff --git a/packages/firebase_chat_repository/pubspec.yaml b/packages/firebase_chat_repository/pubspec.yaml index d67d044..cf291da 100644 --- a/packages/firebase_chat_repository/pubspec.yaml +++ b/packages/firebase_chat_repository/pubspec.yaml @@ -14,41 +14,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages +flutter: \ No newline at end of file diff --git a/packages/flutter_chat/.gitignore b/packages/flutter_chat/.gitignore index ac5aa98..49a187c 100644 --- a/packages/flutter_chat/.gitignore +++ b/packages/flutter_chat/.gitignore @@ -19,7 +19,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. diff --git a/packages/flutter_chat/analysis_options.yaml b/packages/flutter_chat/analysis_options.yaml index a5744c1..bbf71fe 100644 --- a/packages/flutter_chat/analysis_options.yaml +++ b/packages/flutter_chat/analysis_options.yaml @@ -1,4 +1,9 @@ -include: package:flutter_lints/flutter.yaml +include: package:flutter_iconica_analysis/components_options.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: \ No newline at end of file diff --git a/packages/flutter_chat/example/lib/main.dart b/packages/flutter_chat/example/lib/main.dart index 8bfb96e..c2a0dca 100644 --- a/packages/flutter_chat/example/lib/main.dart +++ b/packages/flutter_chat/example/lib/main.dart @@ -1,26 +1,41 @@ -import "package:flutter/material.dart"; -import "package:flutter_chat/flutter_chat.dart"; +import 'package:flutter/material.dart'; +import 'package:flutter_chat/flutter_chat.dart'; -void main(List args) async { - WidgetsFlutterBinding.ensureInitialized(); - - runApp(const App()); +void main() { + runApp(const MyApp()); } -class App extends StatelessWidget { - const App({super.key}); +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override - Widget build(BuildContext context) => const MaterialApp( - home: Home(), - ); + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + useMaterial3: true, + ), + home: const MyHomePage(), + ); + } } -class Home extends StatelessWidget { - const Home({super.key}); +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); @override - Widget build(BuildContext context) => const Center( - child: FlutterChatEntryWidget(userId: '1'), - ); + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: const Center(), + floatingActionButton: const FlutterChatEntryWidget( + userId: '1', + ), + ); + } } diff --git a/packages/flutter_chat/example/pubspec.yaml b/packages/flutter_chat/example/pubspec.yaml index ffe8be3..11f06ec 100644 --- a/packages/flutter_chat/example/pubspec.yaml +++ b/packages/flutter_chat/example/pubspec.yaml @@ -1,40 +1,16 @@ name: example description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. +publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=3.4.3 <4.0.0' + sdk: ^3.5.0 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.6 + cupertino_icons: ^1.0.8 flutter_chat: path: ../ @@ -42,51 +18,7 @@ dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^3.0.0 + flutter_lints: ^4.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_chat/example/test/widget_test.dart b/packages/flutter_chat/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/packages/flutter_chat/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/packages/flutter_chat/lib/flutter_chat.dart b/packages/flutter_chat/lib/flutter_chat.dart index b208ad3..72f5e02 100644 --- a/packages/flutter_chat/lib/flutter_chat.dart +++ b/packages/flutter_chat/lib/flutter_chat.dart @@ -1,11 +1,22 @@ -library flutter_chat; +// ignore_for_file: prefer_double_quotes // Core export 'package:chat_repository_interface/chat_repository_interface.dart'; -// Screens -export "src/config/chat_options.dart"; - // User story export "package:flutter_chat/src/flutter_chat_entry_widget.dart"; export "package:flutter_chat/src/flutter_chat_navigator_userstory.dart"; + +// Options +export "src/config/chat_builders.dart"; +export "src/config/chat_options.dart"; +export "src/config/chat_translations.dart"; + +// Screens +export "src/screens/chat_detail_screen.dart"; +export "src/screens/chat_profile_screen.dart"; +export "src/screens/chat_screen.dart"; +export "src/screens/creation/new_chat_screen.dart"; +export "src/screens/creation/new_group_chat_overview.dart"; +export "src/screens/creation/new_group_chat_screen.dart"; +export "src/services/date_formatter.dart"; diff --git a/packages/flutter_chat/lib/src/config/chat_builders.dart b/packages/flutter_chat/lib/src/config/chat_builders.dart index 646d35d..f093d96 100644 --- a/packages/flutter_chat/lib/src/config/chat_builders.dart +++ b/packages/flutter_chat/lib/src/config/chat_builders.dart @@ -1,8 +1,10 @@ -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_translations.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_translations.dart"; +/// The chat builders class ChatBuilders { + /// The chat builders constructor const ChatBuilders({ this.chatScreenScaffoldBuilder, this.newChatScreenScaffoldBuilder, @@ -23,76 +25,112 @@ class ChatBuilders { this.loadingWidgetBuilder, }); + /// The chat screen scaffold builder final ScaffoldBuilder? chatScreenScaffoldBuilder; + + /// The new chat screen scaffold builder final ScaffoldBuilder? newChatScreenScaffoldBuilder; + + /// The new group chat overview scaffold builder final ScaffoldBuilder? newGroupChatOverviewScaffoldBuilder; + + /// The new group chat screen scaffold builder final ScaffoldBuilder? newGroupChatScreenScaffoldBuilder; + + /// The chat detail scaffold builder final ScaffoldBuilder? chatDetailScaffoldBuilder; + + /// The chat profile scaffold builder final ScaffoldBuilder? chatProfileScaffoldBuilder; + /// The message input builder final TextInputBuilder? messageInputBuilder; + /// The chat row container builder final ContainerBuilder? chatRowContainerBuilder; + /// The group avatar builder final GroupAvatarBuilder? groupAvatarBuilder; + /// The user avatar builder final UserAvatarBuilder? userAvatarBuilder; + /// The delete chat dialog builder final Future Function(BuildContext, ChatModel)? deleteChatDialogBuilder; + /// The new chat button builder final ButtonBuilder? newChatButtonBuilder; + /// The no users placeholder builder final NoUsersPlaceholderBuilder? noUsersPlaceholderBuilder; + /// The chat title builder final Widget Function(String chatTitle)? chatTitleBuilder; + /// The username builder final Widget Function(String userFullName)? usernameBuilder; + /// The image picker container builder final ImagePickerContainerBuilder? imagePickerContainerBuilder; + /// The loading widget builder final Widget? Function(BuildContext context)? loadingWidgetBuilder; } +/// The button builder typedef ButtonBuilder = Widget Function( BuildContext context, VoidCallback onPressed, ChatTranslations translations, ); +/// The image picker container builder typedef ImagePickerContainerBuilder = Widget Function( + BuildContext context, VoidCallback onClose, ChatTranslations translations, - BuildContext context, ); +/// The text input builder typedef TextInputBuilder = Widget Function( + BuildContext context, TextEditingController textEditingController, Widget suffixIcon, ChatTranslations translations, ); +/// The scaffold builder typedef ScaffoldBuilder = Scaffold Function( - AppBar appBar, + BuildContext context, + PreferredSizeWidget appBar, Widget body, Color backgroundColor, ); +/// The container builder typedef ContainerBuilder = Widget Function( + BuildContext context, Widget child, ); +/// The group avatar builder typedef GroupAvatarBuilder = Widget Function( + BuildContext context, String groupName, String? imageUrl, double size, ); +/// The user avatar builder typedef UserAvatarBuilder = Widget Function( + BuildContext context, UserModel user, double size, ); +/// The no users placeholder builder typedef NoUsersPlaceholderBuilder = Widget Function( + BuildContext context, ChatTranslations translations, ); diff --git a/packages/flutter_chat/lib/src/config/chat_options.dart b/packages/flutter_chat/lib/src/config/chat_options.dart index 8015ddc..df45cae 100644 --- a/packages/flutter_chat/lib/src/config/chat_options.dart +++ b/packages/flutter_chat/lib/src/config/chat_options.dart @@ -1,20 +1,13 @@ -import 'dart:ui'; +import "dart:ui"; -import 'package:flutter_chat/src/config/chat_builders.dart'; -import 'package:flutter_chat/src/config/chat_translations.dart'; +import "package:flutter_chat/src/config/chat_builders.dart"; +import "package:flutter_chat/src/config/chat_translations.dart"; +/// The chat options +/// Use this class to configure the chat options. class ChatOptions { - final String Function(bool showFullDate, DateTime date)? dateformat; - final ChatTranslations translations; - final ChatBuilders builders; - final bool groupChatEnabled; - final bool showTimes; - final Color iconEnabledColor; - final Color iconDisabledColor; - final Function? onNoChats; - final int pageSize; - - ChatOptions({ + /// The chat options constructor + const ChatOptions({ this.dateformat, this.groupChatEnabled = true, this.showTimes = true, @@ -25,4 +18,32 @@ class ChatOptions { this.onNoChats, this.pageSize = 20, }); + + /// [dateformat] is a function that formats the date. + // ignore: avoid_positional_boolean_parameters + final String Function(bool showFullDate, DateTime date)? dateformat; + + /// [translations] is the chat translations. + final ChatTranslations translations; + + /// [builders] is the chat builders. + final ChatBuilders builders; + + /// [groupChatEnabled] is a boolean that indicates if group chat is enabled. + final bool groupChatEnabled; + + /// [showTimes] is a boolean that indicates if the chat times are shown. + final bool showTimes; + + /// [iconEnabledColor] is the color of the enabled icon. + final Color iconEnabledColor; + + /// [iconDisabledColor] is the color of the disabled icon. + final Color iconDisabledColor; + + /// [onNoChats] is a function that is triggered when there are no chats. + final Function? onNoChats; + + /// [pageSize] is the number of chats to load at a time. + final int pageSize; } diff --git a/packages/flutter_chat/lib/src/config/chat_translations.dart b/packages/flutter_chat/lib/src/config/chat_translations.dart index 45a5c52..2113167 100644 --- a/packages/flutter_chat/lib/src/config/chat_translations.dart +++ b/packages/flutter_chat/lib/src/config/chat_translations.dart @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: BSD-3-Clause +// ignore_for_file: public_member_api_docs + /// Class that holds all the translations for the chat component view and /// the corresponding userstory class ChatTranslations { diff --git a/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart b/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart index 449c147..87c4385 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart @@ -61,14 +61,14 @@ class _FlutterChatEntryWidgetState extends State { } @override - Widget build(BuildContext context) => GestureDetector( + Widget build(BuildContext context) => InkWell( onTap: () async => widget.onTap?.call() ?? Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => FlutterChatNavigatorUserstory( userId: widget.userId, - chatService: chatService!, + chatService: chatService, ), ), ), 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 7fbe759..deee7a0 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart @@ -2,8 +2,11 @@ // // SPDX-License-Identifier: BSD-3-Clause +import "dart:async"; + +import "package:chat_repository_interface/chat_repository_interface.dart"; import "package:flutter/material.dart"; -import "package:flutter_chat/flutter_chat.dart"; +import "package:flutter_chat/src/config/chat_options.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"; @@ -11,17 +14,27 @@ 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"; +/// The flutter chat navigator userstory +/// [userId] is the id of the user +/// [chatService] is the chat service +/// [chatOptions] are the chat options +/// This widget is the entry point for the chat UI class FlutterChatNavigatorUserstory extends StatefulWidget { + /// Constructs a [FlutterChatNavigatorUserstory]. const FlutterChatNavigatorUserstory({ - super.key, required this.userId, this.chatService, this.chatOptions, + super.key, }); + /// The user ID of the person currently looking at the chat final String userId; + /// The chat service associated with the widget. final ChatService? chatService; + + /// The chat options final ChatOptions? chatOptions; @override @@ -37,112 +50,147 @@ class _FlutterChatNavigatorUserstoryState @override void initState() { chatService = widget.chatService ?? ChatService(); - chatOptions = widget.chatOptions ?? ChatOptions(); + chatOptions = widget.chatOptions ?? const ChatOptions(); super.initState(); } @override - Widget build(BuildContext context) => chatScreen(); + Widget build(BuildContext context) => Navigator( + key: const ValueKey( + "chat_navigator", + ), + onGenerateRoute: (settings) => MaterialPageRoute( + builder: (context) => _NavigatorWrapper( + userId: widget.userId, + chatService: chatService, + chatOptions: chatOptions, + ), + ), + ); +} - Widget chatScreen() { - return ChatScreen( - userId: widget.userId, - chatService: chatService, - chatOptions: chatOptions, - onPressChat: (chat) { - return route(chatDetailScreen(chat)); - }, - onDeleteChat: (chat) { - chatService.deleteChat(chatId: chat.id); - }, - onPressStartChat: () { - return route(newChatScreen()); - }, - ); - } +class _NavigatorWrapper extends StatelessWidget { + const _NavigatorWrapper({ + required this.userId, + required this.chatService, + required this.chatOptions, + }); - Widget chatDetailScreen(ChatModel chat) => ChatDetailScreen( - userId: widget.userId, + final String userId; + final ChatService chatService; + final ChatOptions chatOptions; + + @override + Widget build(BuildContext context) => chatScreen(context); + + Widget chatScreen(BuildContext context) => ChatScreen( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + onPressChat: (chat) => route(context, chatDetailScreen(context, chat)), + onDeleteChat: (chat) async { + await chatService.deleteChat(chatId: chat.id); + }, + onPressStartChat: () => route(context, newChatScreen(context)), + ); + + Widget chatDetailScreen(BuildContext context, ChatModel chat) => + ChatDetailScreen( + userId: userId, chatService: chatService, chatOptions: chatOptions, chat: chat, - onReadChat: (chat) => chatService.markAsRead( + onReadChat: (chat) async => chatService.markAsRead( chatId: chat.id, ), - onPressChatTitle: (chat) { + onPressChatTitle: (chat) async { if (chat.isGroupChat) { - return route(chatProfileScreen(null, chat)); + return route(context, chatProfileScreen(context, null, chat)); } - var otherUser = chat.getOtherUser(widget.userId); + var otherUserId = chat.getOtherUser(userId); + var otherUser = await chatService.getUser(userId: otherUserId).first; - return route(chatProfileScreen(otherUser, null)); - }, - onPressUserProfile: (user) { - return route(chatProfileScreen(user, null)); + if (!context.mounted) return; + return route(context, chatProfileScreen(context, otherUser, null)); }, + onPressUserProfile: (user) => + route(context, chatProfileScreen(context, user, null)), onUploadImage: (data) async { - var path = await chatService.uploadImage(path: 'chats', image: data); + var path = await chatService.uploadImage(path: "chats", image: data); - chatService.sendMessage( + await chatService.sendMessage( + messageId: "${chat.id}-$userId-${DateTime.now()}", chatId: chat.id, - senderId: widget.userId, + senderId: userId, imageUrl: path, ); }, - onMessageSubmit: (text) { - chatService.sendMessage( + onMessageSubmit: (text) async { + await chatService.sendMessage( + messageId: "${chat.id}-$userId-${DateTime.now()}", chatId: chat.id, - senderId: widget.userId, + senderId: userId, text: text, ); }, ); - Widget chatProfileScreen(UserModel? user, ChatModel? chat) => + Widget chatProfileScreen( + BuildContext context, + UserModel? user, + ChatModel? chat, + ) => ChatProfileScreen( + service: chatService, options: chatOptions, - userId: widget.userId, + userId: userId, userModel: user, chatModel: chat, - onTapUser: (user) { - route(chatProfileScreen(user, null)); + onTapUser: (userId) async { + var user = await chatService.getUser(userId: userId).first; + + if (!context.mounted) return; + route(context, chatProfileScreen(context, user, null)); }, - onPressStartChat: (user) async { - var chat = await createChat(user.id); - return route(chatDetailScreen(chat)); + onPressStartChat: (userId) async { + var chat = await createChat(userId); + + if (!context.mounted) return; + return route(context, chatDetailScreen(context, chat)); }, ); - Widget newChatScreen() => NewChatScreen( - userId: widget.userId, + Widget newChatScreen(BuildContext context) => NewChatScreen( + userId: userId, chatService: chatService, chatOptions: chatOptions, - onPressCreateGroupChat: () { - return route(newGroupChatScreen()); - }, + onPressCreateGroupChat: () => + route(context, newGroupChatScreen(context)), onPressCreateChat: (user) async { var chat = await createChat(user.id); - return route(chatDetailScreen(chat)); + + if (!context.mounted) return; + return route(context, chatDetailScreen(context, chat)); }, ); - Widget newGroupChatScreen() => NewGroupChatScreen( - userId: widget.userId, + Widget newGroupChatScreen(BuildContext context) => NewGroupChatScreen( + userId: userId, chatService: chatService, chatOptions: chatOptions, - onContinue: (users) { - return route(newGroupChatOverview(users)); - }, + onContinue: (users) => + route(context, newGroupChatOverview(context, users)), ); - Widget newGroupChatOverview(List users) => NewGroupChatOverview( + Widget newGroupChatOverview(BuildContext context, List users) => + NewGroupChatOverview( options: chatOptions, users: users, onComplete: (users, title, description, image) async { String? path; if (image != null) { - path = await chatService.uploadImage(path: 'groups', image: image); + path = await chatService.uploadImage(path: "groups", image: image); } var chat = await createGroupChat( users, @@ -150,7 +198,9 @@ class _FlutterChatNavigatorUserstoryState description, path, ); - return route(chatDetailScreen(chat)); + + if (!context.mounted) return; + return route(context, chatDetailScreen(context, chat)); }, ); @@ -161,30 +211,43 @@ class _FlutterChatNavigatorUserstoryState String? imageUrl, ) async { ChatModel? chat; - try { chat = await chatService.getGroupChatByUser( - currentUser: widget.userId, + currentUser: userId, otherUsers: userModels, chatName: title, description: description, ); - } catch (e) { + } on Exception catch (_) { chat = null; } if (chat == null) { - var currentUser = await chatService.getUser(userId: widget.userId).first; + var currentUser = await chatService.getUser(userId: userId).first; var otherUsers = await Future.wait( userModels.map((e) => chatService.getUser(userId: e.id).first), ); - chat = await chatService.createChat( + await chatService.createChat( + isGroupChat: true, users: [currentUser, ...otherUsers], chatName: title, description: description, imageUrl: imageUrl, - ).first; + ); + + var chat = await chatService.getGroupChatByUser( + currentUser: userId, + otherUsers: otherUsers, + chatName: title, + description: description, + ); + + if (chat == null) { + throw Exception("Chat not created"); + } + + return chat; } return chat; @@ -195,28 +258,42 @@ class _FlutterChatNavigatorUserstoryState try { chat = await chatService.getChatByUser( - currentUser: widget.userId, + currentUser: userId, otherUser: otherUserId, ); - } catch (e) { + } on Exception catch (_) { chat = null; } if (chat == null) { - var currentUser = await chatService.getUser(userId: widget.userId).first; + var currentUser = await chatService.getUser(userId: userId).first; var otherUser = await chatService.getUser(userId: otherUserId).first; - chat = await chatService.createChat( + await chatService.createChat( + isGroupChat: false, users: [currentUser, otherUser], - ).first; + ); + + var chat = await chatService.getChatByUser( + currentUser: userId, + otherUser: otherUserId, + ); + + if (chat == null) { + throw Exception("Chat not created"); + } + + return chat; } return chat; } - void route(Widget screen) { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => screen), + void route(BuildContext context, Widget screen) { + unawaited( + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => screen), + ), ); } } diff --git a/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart b/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart index bd1f55d..d5529ba 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart @@ -1,16 +1,19 @@ -import 'dart:typed_data'; +import "dart:async"; +import "dart:typed_data"; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/image_picker.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_chat/src/services/date_formatter.dart'; -import 'package:flutter_profile/flutter_profile.dart'; +import "package:cached_network_image/cached_network_image.dart"; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_chat/src/screens/creation/widgets/image_picker.dart"; +import "package:flutter_chat/src/services/date_formatter.dart"; +import "package:flutter_profile/flutter_profile.dart"; +/// Chat detail screen +/// Seen when a user clicks on a chat class ChatDetailScreen extends StatefulWidget { + /// Constructs a [ChatDetailScreen]. const ChatDetailScreen({ - super.key, required this.userId, required this.chatService, required this.chatOptions, @@ -20,16 +23,34 @@ class ChatDetailScreen extends StatefulWidget { required this.onUploadImage, required this.onMessageSubmit, required this.onReadChat, + super.key, }); + /// The user ID of the person currently looking at the chat final String userId; + + /// The chat service associated with the widget. final ChatService chatService; + + /// The chat options final ChatOptions chatOptions; + + /// The chat model currently being viewed final ChatModel chat; + + /// Callback function triggered when the chat title is pressed. final Function(ChatModel) onPressChatTitle; + + /// Callback function triggered when the user profile is pressed. final Function(UserModel) onPressUserProfile; + + /// Callback function triggered when an image is uploaded. final Function(Uint8List image) onUploadImage; + + /// Callback function triggered when a message is submitted. final Function(String text) onMessageSubmit; + + /// Callback function triggered when the chat is read. final Function(ChatModel chat) onReadChat; @override @@ -37,7 +58,7 @@ class ChatDetailScreen extends StatefulWidget { } class _ChatDetailScreenState extends State { - late String chatTitle; + String? chatTitle; @override void initState() { @@ -45,25 +66,35 @@ class _ChatDetailScreenState extends State { chatTitle = widget.chat.chatName ?? widget.chatOptions.translations.groupNameEmpty; } else { - chatTitle = widget.chat.users - .firstWhere((element) => element.id != widget.userId) - .fullname ?? - widget.chatOptions.translations.anonymousUser; + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _getTitle(); + }); } super.initState(); } + Future _getTitle() async { + var userId = + widget.chat.users.firstWhere((element) => element != widget.userId); + var user = await widget.chatService.getUser(userId: userId).first; + + chatTitle = user.fullname ?? widget.chatOptions.translations.anonymousUser; + + setState(() {}); + } + @override Widget build(BuildContext context) { var theme = Theme.of(context); return widget.chatOptions.builders.chatDetailScaffoldBuilder?.call( + context, _AppBar( chatTitle: chatTitle, chatOptions: widget.chatOptions, onPressChatTitle: widget.onPressChatTitle, chatModel: widget.chat, - ) as AppBar, + ), _Body( chatService: widget.chatService, options: widget.chatOptions, @@ -105,7 +136,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { required this.chatModel, }); - final String chatTitle; + final String? chatTitle; final ChatOptions chatOptions; final Function(ChatModel) onPressChatTitle; final ChatModel chatModel; @@ -128,9 +159,9 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { ), title: GestureDetector( onTap: () => onPressChatTitle.call(chatModel), - child: chatOptions.builders.chatTitleBuilder?.call(chatTitle) ?? + child: chatOptions.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? Text( - chatTitle, + chatTitle ?? "", overflow: TextOverflow.ellipsis, ), ), @@ -167,10 +198,10 @@ class _Body extends StatefulWidget { } class _BodyState extends State<_Body> { - ScrollController controller = ScrollController(); + final ScrollController controller = ScrollController(); bool showIndicator = false; late int pageSize; - var page = 0; + int page = 0; @override void initState() { @@ -178,78 +209,86 @@ class _BodyState extends State<_Body> { super.initState(); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { var theme = Theme.of(context); + void handleScroll(PointerMoveEvent event) { + if (!showIndicator && + controller.offset >= controller.position.maxScrollExtent && + !controller.position.outOfRange) { + setState(() { + showIndicator = true; + }); + + setState(() { + page++; + }); + + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + showIndicator = false; + }); + } + }); + } + } + return Stack( children: [ Column( children: [ Expanded( child: StreamBuilder?>( - stream: widget.chatService.getMessages( - userId: widget.currentUserId, - chatId: widget.chat.id, - pageSize: pageSize, - page: page, - ), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } + stream: widget.chatService.getMessages( + userId: widget.currentUserId, + chatId: widget.chat.id, + pageSize: pageSize, + page: page, + ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } - var messages = snapshot.data?.reversed.toList() ?? []; + var messages = snapshot.data?.reversed.toList() ?? []; - WidgetsBinding.instance.addPostFrameCallback((_) async { - await widget.onReadChat(widget.chat); - }); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await widget.onReadChat(widget.chat); + }); - return Listener( - onPointerMove: (event) { - if (!showIndicator && - controller.offset >= - controller.position.maxScrollExtent && - !controller.position.outOfRange) { - setState(() { - showIndicator = true; - }); - - setState(() { - page++; - }); - - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - showIndicator = false; - }); - } - }); - } - }, - child: ListView( - shrinkWrap: true, - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - reverse: messages.isNotEmpty, - padding: const EdgeInsets.only(top: 24.0), - children: [ - if (messages.isEmpty && !showIndicator) ...[ - Center( - child: Text( - widget.chat.isGroupChat - ? widget.options.translations - .writeFirstMessageInGroupChat - : widget.options.translations - .writeMessageToStartChat, - style: theme.textTheme.bodySmall, - ), + return Listener( + onPointerMove: handleScroll, + child: ListView( + shrinkWrap: true, + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + reverse: messages.isNotEmpty, + padding: const EdgeInsets.only(top: 24.0), + children: [ + if (messages.isEmpty && !showIndicator) ...[ + Center( + child: Text( + widget.chat.isGroupChat + ? widget.options.translations + .writeFirstMessageInGroupChat + : widget.options.translations + .writeMessageToStartChat, + style: theme.textTheme.bodySmall, ), - ], - for (var i = 0; i < messages.length; i++) ...[ + ), + ], + for (var i = 0; i < messages.length; i++) ...[ + if (widget.chat.id == messages[i].chatId) ...[ _ChatBubble( key: ValueKey(messages[i].id), message: messages[i], @@ -260,11 +299,13 @@ class _BodyState extends State<_Body> { onPressUserProfile: widget.onPressUserProfile, options: widget.options, ), - ] + ], ], - ), - ); - }), + ], + ), + ); + }, + ), ), _ChatBottom( chat: widget.chat, @@ -350,6 +391,7 @@ class _ChatBottomState extends State<_ChatBottom> { child: SizedBox( height: 45, child: widget.options.builders.messageInputBuilder?.call( + context, _textEditingController, Row( mainAxisSize: MainAxisSize.min, @@ -412,9 +454,7 @@ class _ChatBottomState extends State<_ChatBottom> { horizontal: 30, ), hintText: widget.options.translations.messagePlaceholder, - hintStyle: theme.textTheme.bodyMedium!.copyWith( - color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5), - ), + hintStyle: theme.textTheme.bodyMedium, fillColor: Colors.white, filled: true, border: const OutlineInputBorder( @@ -502,135 +542,135 @@ class _ChatBubbleState extends State<_ChatBubble> { widget.previousMessage?.timestamp.minute; var hasHeader = isNewDate || isSameSender; return StreamBuilder( - stream: widget.chatService.getUser(userId: widget.message.senderId), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } + stream: widget.chatService.getUser(userId: widget.message.senderId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } - var user = snapshot.data!; + var user = snapshot.data!; - return Padding( - padding: EdgeInsets.only( - top: isNewDate || isSameSender ? 25.0 : 0, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isNewDate || isSameSender) ...[ - GestureDetector( - onTap: () => widget.onPressUserProfile(user), - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: user.imageUrl?.isNotEmpty ?? false - ? _ChatImage( - image: user.imageUrl!, - ) - : widget.options.builders.userAvatarBuilder?.call( - user, - 40, - ) ?? - Avatar( - key: ValueKey(user.id), - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl != "" - ? user.imageUrl - : null, - ), - size: 40, - ), - ), - ), - ] else ...[ - const SizedBox( - width: 50, - ), - ], - Expanded( + return Padding( + padding: EdgeInsets.only( + top: isNewDate || isSameSender ? 25.0 : 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isNewDate || isSameSender) ...[ + InkWell( + onTap: () => widget.onPressUserProfile(user), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 22.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isNewDate || isSameSender) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: widget.options.builders.usernameBuilder - ?.call( - user.fullname ?? "", - ) ?? - Text( - user.fullname ?? - translations.anonymousUser, - style: theme.textTheme.titleMedium, - ), + padding: const EdgeInsets.only(left: 10.0), + child: user.imageUrl?.isNotEmpty ?? false + ? _ChatImage( + image: user.imageUrl!, + ) + : widget.options.builders.userAvatarBuilder?.call( + context, + user, + 40, + ) ?? + Avatar( + key: ValueKey(user.id), + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: + user.imageUrl != "" ? user.imageUrl : null, ), - Padding( - padding: const EdgeInsets.only(top: 5.0), - child: Text( - dateFormatter.format( - date: widget.message.timestamp, - showFullDate: true, - ), - style: theme.textTheme.labelSmall, - ), - ), - ], - ), - ], - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: widget.message.isTextMessage() - ? Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - widget.message.text ?? "", - style: theme.textTheme.bodySmall, - ), - ), - if (widget.options.showTimes && - !isSameMinute && - !isNewDate && - !hasHeader) - Text( - dateFormatter - .format( - date: widget.message.timestamp, - showFullDate: true, - ) - .split(" ") - .last, - style: theme.textTheme.labelSmall, - textAlign: TextAlign.end, - ), - ], - ) - : widget.message.isImageMessage() - ? CachedNetworkImage( - imageUrl: widget.message.imageUrl ?? "", - ) - : const SizedBox.shrink(), - ), - ], - ), + size: 40, + ), ), ), + ] else ...[ + const SizedBox( + width: 50, + ), ], - ), - ); - }); + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isNewDate || isSameSender) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: widget.options.builders.usernameBuilder + ?.call( + user.fullname ?? "", + ) ?? + Text( + user.fullname ?? translations.anonymousUser, + style: theme.textTheme.titleMedium, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text( + dateFormatter.format( + date: widget.message.timestamp, + showFullDate: true, + ), + style: theme.textTheme.labelSmall, + ), + ), + ], + ), + ], + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: widget.message.isTextMessage + ? Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + widget.message.text ?? "", + style: theme.textTheme.bodySmall, + ), + ), + if (widget.options.showTimes && + !isSameMinute && + !isNewDate && + !hasHeader) + Text( + dateFormatter + .format( + date: widget.message.timestamp, + showFullDate: true, + ) + .split(" ") + .last, + style: theme.textTheme.labelSmall, + textAlign: TextAlign.end, + ), + ], + ) + : widget.message.isImageMessage + ? CachedNetworkImage( + imageUrl: widget.message.imageUrl ?? "", + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); } } 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 8e451ef..fb0f6cf 100644 --- a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart @@ -1,37 +1,57 @@ -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_profile/flutter_profile.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_profile/flutter_profile.dart"; +/// The chat profile screen +/// Seen when a user taps on a chat profile +/// Also used for group chats class ChatProfileScreen extends StatelessWidget { + /// Constructs a [ChatProfileScreen] const ChatProfileScreen({ - super.key, required this.options, required this.userId, required this.userModel, + required this.service, required this.chatModel, required this.onTapUser, required this.onPressStartChat, + super.key, }); + /// The chat options final ChatOptions options; + + /// The user ID of the person currently looking at the chat final String userId; + + /// The user model of the persons profile to be viewed final UserModel? userModel; + + /// The chat model of the chat being viewed final ChatModel? chatModel; - final Function(UserModel)? onTapUser; - final Function(UserModel)? onPressStartChat; + + /// Callback function triggered when a user is tapped + final Function(String)? onTapUser; + + final ChatService service; + + /// Callback function triggered when the start chat button is pressed + final Function(String)? onPressStartChat; @override Widget build(BuildContext context) { var theme = Theme.of(context); return options.builders.chatProfileScaffoldBuilder?.call( + context, _AppBar( user: userModel, chat: chatModel, options: options, - ) as AppBar, + ), _Body( + service: service, currentUser: userId, options: options, user: userModel, @@ -50,6 +70,7 @@ class ChatProfileScreen extends StatelessWidget { body: _Body( currentUser: userId, options: options, + service: service, user: userModel, chat: chatModel, onTapUser: onTapUser, @@ -78,7 +99,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { const IconThemeData(color: Colors.white), title: Text( user != null - ? '${user!.fullname}' + ? "${user!.fullname}" : chat != null ? chat?.chatName ?? options.translations.groupNameEmpty : "", @@ -93,6 +114,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { class _Body extends StatelessWidget { const _Body({ required this.options, + required this.service, required this.user, required this.chat, required this.onPressStartChat, @@ -101,10 +123,11 @@ class _Body extends StatelessWidget { }); final ChatOptions options; + final ChatService service; final UserModel? user; final ChatModel? chat; - final Function(UserModel)? onTapUser; - final Function(UserModel)? onPressStartChat; + final Function(String)? onTapUser; + final Function(String)? onPressStartChat; final String currentUser; @override @@ -119,6 +142,7 @@ class _Body extends StatelessWidget { child: Column( children: [ options.builders.userAvatarBuilder?.call( + context, user ?? ( chat != null @@ -185,8 +209,7 @@ class _Body extends StatelessWidget { ), Text( chat!.description ?? "", - style: theme.textTheme.bodyMedium! - .copyWith(color: Colors.black), + style: theme.textTheme.bodyMedium, ), const SizedBox( height: 12, @@ -206,7 +229,7 @@ class _Body extends StatelessWidget { bottom: 8, right: 8, ), - child: GestureDetector( + child: InkWell( onTap: () { onTapUser?.call(tappedUser); }, @@ -214,23 +237,42 @@ class _Body extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - options.builders.userAvatarBuilder?.call( - tappedUser, - 44, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: tappedUser.firstName, - lastName: tappedUser.lastName, - imageUrl: - tappedUser.imageUrl != null || - tappedUser.imageUrl != "" - ? tappedUser.imageUrl + 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, - ), + ), + size: 60, + ); + }, + ), ], ), ), @@ -244,7 +286,7 @@ class _Body extends StatelessWidget { ], ], ), - if (user != null && user!.id != currentUser) ...[ + if (user?.id != currentUser) ...[ Align( alignment: Alignment.bottomCenter, child: Padding( @@ -254,7 +296,7 @@ class _Body extends StatelessWidget { ), child: FilledButton( onPressed: () { - onPressStartChat?.call(user!); + onPressStartChat?.call(user!.id); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/packages/flutter_chat/lib/src/screens/chat_screen.dart b/packages/flutter_chat/lib/src/screens/chat_screen.dart index a330089..8939730 100644 --- a/packages/flutter_chat/lib/src/screens/chat_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_screen.dart @@ -1,23 +1,31 @@ -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_chat/src/config/chat_translations.dart'; -import 'package:flutter_chat/src/services/date_formatter.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_chat/src/config/chat_translations.dart"; +import "package:flutter_chat/src/services/date_formatter.dart"; import "package:flutter_profile/flutter_profile.dart"; +/// The chat screen +/// Seen when a user is chatting class ChatScreen extends StatelessWidget { + /// Constructs a [ChatScreen] const ChatScreen({ - super.key, required this.userId, required this.chatService, required this.chatOptions, required this.onPressChat, required this.onDeleteChat, this.onPressStartChat, + super.key, }); + /// The user ID of the person currently looking at the chat final String userId; + + /// The chat service final ChatService chatService; + + /// The chat options final ChatOptions chatOptions; /// Callback function for starting a chat. @@ -26,17 +34,19 @@ class ChatScreen extends StatelessWidget { /// Callback function for pressing on a chat. final void Function(ChatModel chat) onPressChat; + /// Callback function for deleting a chat. final void Function(ChatModel chat) onDeleteChat; @override Widget build(BuildContext context) { var theme = Theme.of(context); return chatOptions.builders.chatScreenScaffoldBuilder?.call( + context, _AppBar( userId: userId, chatOptions: chatOptions, chatService: chatService, - ) as AppBar, + ), _Body( userId: userId, chatOptions: chatOptions, @@ -134,7 +144,7 @@ class _Body extends StatefulWidget { } class _BodyState extends State<_Body> { - ScrollController controller = ScrollController(); + final ScrollController controller = ScrollController(); bool _hasCalledOnNoChats = false; @override @@ -152,7 +162,6 @@ class _BodyState extends State<_Body> { StreamBuilder?>( stream: widget.chatService.getChats(userId: widget.userId), builder: (BuildContext context, snapshot) { - // if the stream is done, empty and noChats is set we should call that if (snapshot.connectionState == ConnectionState.done && (snapshot.data?.isEmpty ?? true) || (snapshot.data != null && snapshot.data!.isEmpty)) { @@ -160,6 +169,7 @@ class _BodyState extends State<_Body> { !_hasCalledOnNoChats) { _hasCalledOnNoChats = true; // Set the flag to true WidgetsBinding.instance.addPostFrameCallback((_) async { + // ignore: avoid_dynamic_calls await widget.chatOptions.onNoChats!.call(); }); } @@ -172,7 +182,7 @@ class _BodyState extends State<_Body> { } return Column( children: [ - for (ChatModel chat in (snapshot.data ?? [])) ...[ + for (ChatModel chat in snapshot.data ?? []) ...[ DecoratedBox( decoration: BoxDecoration( border: Border( @@ -186,17 +196,19 @@ class _BodyState extends State<_Body> { builder: (context) => !chat.canBeDeleted ? Dismissible( confirmDismiss: (_) async { - widget.chatOptions.builders + await widget.chatOptions.builders .deleteChatDialogBuilder ?.call(context, chat) ?? _deleteDialog( chat, translations, + // ignore: use_build_context_synchronously context, ); return _deleteDialog( chat, translations, + // ignore: use_build_context_synchronously context, ); }, @@ -230,16 +242,18 @@ class _BodyState extends State<_Body> { ), ), key: ValueKey( - chat.id.toString(), + chat.id, ), - child: ChatListItem( + child: _ChatItem( + service: widget.chatService, chat: chat, chatOptions: widget.chatOptions, userId: widget.userId, onPressChat: widget.onPressChat, ), ) - : ChatListItem( + : _ChatItem( + service: widget.chatService, chat: chat, chatOptions: widget.chatOptions, userId: widget.userId, @@ -274,7 +288,7 @@ class _BodyState extends State<_Body> { borderRadius: BorderRadius.circular(56), ), ), - onPressed: widget.onPressStartChat!, + onPressed: widget.onPressStartChat, child: Text( translations.newChatButton, style: theme.textTheme.displayLarge, @@ -286,17 +300,18 @@ class _BodyState extends State<_Body> { } } -class ChatListItem extends StatelessWidget { - const ChatListItem({ +class _ChatItem extends StatelessWidget { + const _ChatItem({ required this.chat, required this.chatOptions, + required this.service, required this.userId, required this.onPressChat, - super.key, }); final ChatModel chat; final ChatOptions chatOptions; + final ChatService service; final String userId; final Function(ChatModel chat) onPressChat; @@ -306,15 +321,17 @@ class ChatListItem extends StatelessWidget { options: chatOptions, ); var theme = Theme.of(context); - return GestureDetector( + return InkWell( onTap: () { onPressChat(chat); }, child: chatOptions.builders.chatRowContainerBuilder?.call( + context, _ChatListItem( chat: chat, options: chatOptions, dateFormatter: dateFormatter, + chatService: service, currentUserId: userId, ), ) ?? @@ -334,6 +351,7 @@ class ChatListItem extends StatelessWidget { chat: chat, options: chatOptions, dateFormatter: dateFormatter, + chatService: service, currentUserId: userId, ), ), @@ -348,82 +366,140 @@ class _ChatListItem extends StatelessWidget { required this.options, required this.dateFormatter, required this.currentUserId, + required this.chatService, }); final ChatModel chat; final ChatOptions options; final DateFormatter dateFormatter; final String currentUserId; + final ChatService chatService; @override Widget build(BuildContext context) { var translations = options.translations; if (chat.isGroupChat) { - return _ChatRow( - title: chat.chatName ?? translations.groupNameEmpty, - unreadMessages: chat.unreadMessageCount, - subTitle: chat.lastMessage != null - ? chat.lastMessage!.isTextMessage() - ? chat.lastMessage!.text - : "📷 " - "${translations.image}" - : "", - avatar: options.builders.groupAvatarBuilder?.call( - chat.chatName ?? translations.groupNameEmpty, - chat.imageUrl, - 40.0, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: chat.chatName, - lastName: null, - imageUrl: chat.imageUrl != null || chat.imageUrl != "" - ? chat.imageUrl - : null, - ), - size: 40.0, - ), - lastUsed: chat.lastUsed != null - ? dateFormatter.format( - date: chat.lastUsed!, + return StreamBuilder( + stream: chat.lastMessage != null + ? chatService.getMessage( + chatId: chat.id, + messageId: chat.lastMessage!, ) - : null, + : const Stream.empty(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + var data = snapshot.data; + + return _ChatRow( + title: chat.chatName ?? translations.groupNameEmpty, + unreadMessages: chat.unreadMessageCount, + subTitle: data != null + ? data.isTextMessage + ? data.text + : "📷 " + "${translations.image}" + : "", + avatar: options.builders.groupAvatarBuilder?.call( + context, + chat.chatName ?? translations.groupNameEmpty, + chat.imageUrl, + 40.0, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: chat.chatName, + lastName: null, + imageUrl: chat.imageUrl != null || chat.imageUrl != "" + ? chat.imageUrl + : null, + ), + size: 40.0, + ), + lastUsed: chat.lastUsed != null + ? dateFormatter.format( + date: chat.lastUsed!, + ) + : null, + ); + }, ); } var otherUser = chat.users.firstWhere( - (element) => element.id != currentUserId, + (element) => element != currentUserId, ); - return _ChatRow( - unreadMessages: chat.unreadMessageCount, - avatar: options.builders.userAvatarBuilder?.call( - otherUser, - 40.0, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: otherUser.firstName, - lastName: otherUser.lastName, - imageUrl: otherUser.imageUrl != null || otherUser.imageUrl != "" - ? otherUser.imageUrl + return StreamBuilder( + stream: chatService.getUser(userId: otherUser), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + var otherUser = snapshot.data; + + if (otherUser == null) { + return const SizedBox(); + } + + return StreamBuilder( + stream: chat.lastMessage != null + ? chatService.getMessage( + chatId: chat.id, + messageId: chat.lastMessage!, + ) + : const Stream.empty(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + var data = snapshot.data; + + return _ChatRow( + unreadMessages: chat.unreadMessageCount, + avatar: options.builders.userAvatarBuilder?.call( + context, + otherUser, + 40.0, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: otherUser.firstName, + lastName: otherUser.lastName, + imageUrl: + otherUser.imageUrl != null || otherUser.imageUrl != "" + ? otherUser.imageUrl + : null, + ), + size: 40.0, + ), + title: otherUser.fullname ?? translations.anonymousUser, + subTitle: data != null + ? data.isTextMessage + ? data.text + : "📷 " + "${translations.image}" + : "", + lastUsed: chat.lastUsed != null + ? dateFormatter.format( + date: chat.lastUsed!, + ) : null, - ), - size: 40.0, - ), - title: otherUser.fullname ?? translations.anonymousUser, - subTitle: chat.lastMessage != null - ? chat.lastMessage!.isTextMessage() - ? chat.lastMessage!.text - : "📷 " - "${translations.image}" - : "", - lastUsed: chat.lastUsed != null - ? dateFormatter.format( - date: chat.lastUsed!, - ) - : null, + ); + }, + ); + }, ); } } @@ -490,7 +566,6 @@ class _ChatRow extends StatelessWidget { this.lastUsed, this.subTitle, this.avatar, - super.key, }); /// The title of the chat. @@ -535,11 +610,7 @@ class _ChatRow extends StatelessWidget { padding: const EdgeInsets.only(top: 3.0), child: Text( subTitle!, - style: unreadMessages > 0 - ? theme.textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w800, - ) - : theme.textTheme.bodySmall, + style: theme.textTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 2, ), diff --git a/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart b/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart index e883c1a..58f4465 100644 --- a/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart +++ b/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart @@ -1,11 +1,14 @@ -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/search_field.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/search_icon.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/user_list.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_chat/src/screens/creation/widgets/search_field.dart"; +import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart"; +import "package:flutter_chat/src/screens/creation/widgets/user_list.dart"; +/// New chat screen +/// This screen is used to create a new chat class NewChatScreen extends StatefulWidget { + /// Constructs a [NewChatScreen] const NewChatScreen({ required this.userId, required this.chatService, @@ -15,10 +18,19 @@ class NewChatScreen extends StatefulWidget { super.key, }); + /// The user ID of the person currently looking at the chat final String userId; + + /// The chat service associated with the widget. final ChatService chatService; + + /// The chat options final ChatOptions chatOptions; + + /// Callback function triggered when the create group chat button is pressed final VoidCallback onPressCreateGroupChat; + + /// Callback function triggered when a user is tapped final Function(UserModel) onPressCreateChat; @override @@ -35,6 +47,7 @@ class _NewChatScreenState extends State { var theme = Theme.of(context); return widget.chatOptions.builders.newChatScreenScaffoldBuilder?.call( + context, _AppBar( chatOptions: widget.chatOptions, isSearching: _isSearching, @@ -55,7 +68,7 @@ class _NewChatScreenState extends State { } }, focusNode: _textFieldFocusNode, - ) as AppBar, + ), _Body( chatOptions: widget.chatOptions, chatService: widget.chatService, @@ -218,7 +231,7 @@ class _Body extends StatelessWidget { ); } else { return chatOptions.builders.noUsersPlaceholderBuilder - ?.call(translations) ?? + ?.call(context, translations) ?? Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Align( diff --git a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart index c4309ac..45811f2 100644 --- a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart +++ b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart @@ -1,32 +1,46 @@ -import 'dart:typed_data'; +import "dart:typed_data"; -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/image_picker.dart'; -import 'package:flutter_profile/flutter_profile.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_chat/src/screens/creation/widgets/image_picker.dart"; +import "package:flutter_profile/flutter_profile.dart"; +/// New group chat overview +/// Seen after the user has selected the users they +/// want to add to the group chat class NewGroupChatOverview extends StatelessWidget { + /// Constructs a [NewGroupChatOverview] const NewGroupChatOverview({ - super.key, required this.options, required this.users, required this.onComplete, + super.key, }); + /// The chat options final ChatOptions options; + + /// The users to be added to the group chat final List users; - final Function(List users, String chatName, String description, - Uint8List? image) onComplete; + + /// Callback function triggered when the group chat is created + final Function( + List users, + String chatName, + String description, + Uint8List? image, + ) onComplete; @override Widget build(BuildContext context) { var theme = Theme.of(context); return options.builders.newGroupChatOverviewScaffoldBuilder?.call( + context, _AppBar( options: options, - ) as AppBar, + ), _Body( options: options, users: users, @@ -80,8 +94,12 @@ class _Body extends StatefulWidget { final ChatOptions options; final List users; - final Function(List users, String chatName, String description, - Uint8List? image) onComplete; + final Function( + List users, + String chatName, + String description, + Uint8List? image, + ) onComplete; @override State<_Body> createState() => _BodyState(); @@ -92,10 +110,10 @@ class _BodyState extends State<_Body> { final TextEditingController _bioController = TextEditingController(); Uint8List? image; - var formKey = GlobalKey(); - var isPressed = false; + GlobalKey formKey = GlobalKey(); + bool isPressed = false; - var users = []; + List users = []; @override void initState() { @@ -123,7 +141,7 @@ class _BodyState extends State<_Body> { Center( child: Stack( children: [ - GestureDetector( + InkWell( onTap: () async => onPressSelectImage( context, widget.options, @@ -162,7 +180,7 @@ class _BodyState extends State<_Body> { borderRadius: BorderRadius.circular(40), ), child: Center( - child: GestureDetector( + child: InkWell( onTap: () { setState(() { image = null; @@ -198,10 +216,7 @@ class _BodyState extends State<_Body> { fillColor: Colors.white, filled: true, hintText: translations.groupNameHintText, - hintStyle: theme.textTheme.bodyMedium!.copyWith( - color: - theme.textTheme.bodyMedium!.color!.withOpacity(0.5), - ), + hintStyle: theme.textTheme.bodyMedium, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide( @@ -245,10 +260,7 @@ class _BodyState extends State<_Body> { fillColor: Colors.white, filled: true, hintText: translations.groupBioHintText, - hintStyle: theme.textTheme.bodyMedium!.copyWith( - color: - theme.textTheme.bodyMedium!.color!.withOpacity(0.5), - ), + hintStyle: theme.textTheme.bodyMedium, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide( @@ -357,39 +369,38 @@ class _SelectedUser extends StatelessWidget { final Function(UserModel) onRemove; @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - onRemove(user); - }, - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: options.builders.userAvatarBuilder?.call( - user, - 40, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl != "" ? user.imageUrl : null, + Widget build(BuildContext context) => InkWell( + onTap: () { + onRemove(user); + }, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: options.builders.userAvatarBuilder?.call( + context, + user, + 40, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl != "" ? user.imageUrl : null, + ), + size: 40, ), - size: 40, - ), - ), - Positioned.directional( - textDirection: Directionality.of(context), - end: 0, - child: const Icon( - Icons.cancel, - size: 20, ), - ), - ], - ), - ); - } + Positioned.directional( + textDirection: Directionality.of(context), + end: 0, + child: const Icon( + Icons.cancel, + size: 20, + ), + ), + ], + ), + ); } diff --git a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart index 9dbee2d..7a1503f 100644 --- a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart +++ b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart @@ -1,11 +1,14 @@ -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/search_field.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/search_icon.dart'; -import 'package:flutter_chat/src/screens/creation/widgets/user_list.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_chat/src/screens/creation/widgets/search_field.dart"; +import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart"; +import "package:flutter_chat/src/screens/creation/widgets/user_list.dart"; +/// New group chat screen +/// This screen is used to create a new group chat class NewGroupChatScreen extends StatefulWidget { + /// Constructs a [NewGroupChatScreen] const NewGroupChatScreen({ required this.userId, required this.chatService, @@ -14,9 +17,16 @@ class NewGroupChatScreen extends StatefulWidget { super.key, }); + /// The user ID of the person currently looking at the chat final String userId; + + /// The chat service associated with the widget. final ChatService chatService; + + /// The chat options final ChatOptions chatOptions; + + /// Callback function triggered when the continue button is pressed final Function(List) onContinue; @override @@ -35,6 +45,7 @@ class _NewGroupChatScreenState extends State { var theme = Theme.of(context); return widget.chatOptions.builders.newGroupChatScreenScaffoldBuilder?.call( + context, _AppBar( chatOptions: widget.chatOptions, isSearching: _isSearching, @@ -55,7 +66,7 @@ class _NewGroupChatScreenState extends State { } }, focusNode: _textFieldFocusNode, - ) as AppBar, + ), _Body( onSelectedUser: handleUserTap, selectedUsers: selectedUsers, @@ -219,7 +230,7 @@ class _Body extends StatelessWidget { ); } else { return chatOptions.builders.noUsersPlaceholderBuilder - ?.call(translations) ?? + ?.call(context, translations) ?? Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Align( diff --git a/packages/flutter_chat/lib/src/screens/creation/widgets/image_picker.dart b/packages/flutter_chat/lib/src/screens/creation/widgets/image_picker.dart index 32b2bff..66c4108 100644 --- a/packages/flutter_chat/lib/src/screens/creation/widgets/image_picker.dart +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/image_picker.dart @@ -1,10 +1,11 @@ -import 'dart:typed_data'; +import "dart:typed_data"; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_chat/src/config/chat_translations.dart'; -import 'package:flutter_image_picker/flutter_image_picker.dart'; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_chat/src/config/chat_translations.dart"; +import "package:flutter_image_picker/flutter_image_picker.dart"; +/// The function to call when the user selects an image Future onPressSelectImage( BuildContext context, ChatOptions options, @@ -14,9 +15,9 @@ Future onPressSelectImage( context: context, builder: (BuildContext context) => options.builders.imagePickerContainerBuilder?.call( + context, () => Navigator.of(context).pop(), options.translations, - context, ) ?? Container( padding: const EdgeInsets.all(8.0), @@ -38,9 +39,7 @@ Future onPressSelectImage( onPressed: () => Navigator.of(context).pop(), child: Text( options.translations.cancelImagePickerBtn, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - decoration: TextDecoration.underline, - ), + style: Theme.of(context).textTheme.bodyMedium, ), ), ), diff --git a/packages/flutter_chat/lib/src/screens/creation/widgets/search_field.dart b/packages/flutter_chat/lib/src/screens/creation/widgets/search_field.dart index 064471f..d01d4b8 100644 --- a/packages/flutter_chat/lib/src/screens/creation/widgets/search_field.dart +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/search_field.dart @@ -1,20 +1,31 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +/// The search field widget class SearchField extends StatelessWidget { + /// Constructs a [SearchField] const SearchField({ - super.key, required this.chatOptions, required this.isSearching, required this.onSearch, required this.focusNode, required this.text, + super.key, }); + /// The chat options final ChatOptions chatOptions; + + /// Whether the search field is currently in use final bool isSearching; + + /// Callback function triggered when the search field is used final Function(String query) onSearch; + + /// The focus node of the search field final FocusNode focusNode; + + /// The text to display in the search field final String text; @override @@ -22,25 +33,26 @@ class SearchField extends StatelessWidget { var theme = Theme.of(context); var translations = chatOptions.translations; - return isSearching - ? TextField( - focusNode: focusNode, - onChanged: onSearch, - decoration: InputDecoration( - hintText: translations.searchPlaceholder, - hintStyle: - theme.textTheme.bodyMedium!.copyWith(color: Colors.white), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: theme.colorScheme.primary, - ), - ), + if (isSearching) { + return TextField( + focusNode: focusNode, + onChanged: onSearch, + decoration: InputDecoration( + hintText: translations.searchPlaceholder, + hintStyle: theme.textTheme.bodyMedium, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: theme.colorScheme.primary, ), - style: theme.textTheme.bodySmall!.copyWith(color: Colors.white), - cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, - ) - : Text( - text, - ); + ), + ), + style: theme.textTheme.bodySmall, + cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, + ); + } + + return Text( + text, + ); } } diff --git a/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart b/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart index 2a3cee9..98cbc96 100644 --- a/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart @@ -1,13 +1,18 @@ -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; +/// A widget representing a search icon. class SearchIcon extends StatelessWidget { + /// Constructs a [SearchIcon]. const SearchIcon({ - super.key, required this.isSearching, required this.onPressed, + super.key, }); + /// Whether the search icon is currently in use final bool isSearching; + + /// Callback function triggered when the search icon is pressed final VoidCallback onPressed; @override diff --git a/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart b/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart index 4558c4a..6afc14f 100644 --- a/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart @@ -1,11 +1,12 @@ -import 'package:chat_repository_interface/chat_repository_interface.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat/src/config/chat_options.dart'; -import 'package:flutter_profile/flutter_profile.dart'; +import "package:chat_repository_interface/chat_repository_interface.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; +import "package:flutter_profile/flutter_profile.dart"; +/// The user list widget class UserList extends StatefulWidget { + /// Constructs a [UserList] const UserList({ - super.key, required this.users, required this.currentUser, required this.query, @@ -14,15 +15,31 @@ class UserList extends StatefulWidget { this.creatingGroup = false, this.selectedUsers = const [], this.onSelectedUser, + super.key, }); + /// The list of users final List users; + + /// The query to search for final String query; + + /// The current user final String currentUser; + + /// The chat options final ChatOptions options; + + /// Whether the user is creating a group final bool creatingGroup; + + /// Callback function triggered when a chat is created final Function(UserModel)? onPressCreateChat; + + /// The selected users final List selectedUsers; + + /// Callback function triggered when a user is selected final Function(UserModel)? onSelectedUser; @override @@ -71,10 +88,11 @@ class _UserListState extends State { } }, child: widget.options.builders.chatRowContainerBuilder?.call( + context, Row( children: [ widget.options.builders.userAvatarBuilder - ?.call(user, 44) ?? + ?.call(context, user, 44) ?? Avatar( boxfit: BoxFit.cover, user: User( @@ -122,7 +140,7 @@ class _UserListState extends State { child: Row( children: [ widget.options.builders.userAvatarBuilder - ?.call(user, 44) ?? + ?.call(context, user, 44) ?? Avatar( boxfit: BoxFit.cover, user: User( @@ -162,7 +180,7 @@ class _UserListState extends State { ); } - void handlePersonalChatTap(UserModel user) async { + Future handlePersonalChatTap(UserModel user) async { if (!isPressed) { setState(() { isPressed = true; diff --git a/packages/flutter_chat/lib/src/services/date_formatter.dart b/packages/flutter_chat/lib/src/services/date_formatter.dart index dac9e28..40617ff 100644 --- a/packages/flutter_chat/lib/src/services/date_formatter.dart +++ b/packages/flutter_chat/lib/src/services/date_formatter.dart @@ -5,10 +5,14 @@ import "package:flutter_chat/src/config/chat_options.dart"; import "package:intl/intl.dart"; +/// The date formatter class DateFormatter { + /// Constructs a [DateFormatter] DateFormatter({ required this.options, }); + + /// The chat options final ChatOptions options; final _now = DateTime.now(); @@ -46,6 +50,7 @@ class DateFormatter { bool _isThisYear(DateTime date) => date.year == _now.year; + /// Formats the date String format({ required DateTime date, bool showFullDate = false, diff --git a/packages/flutter_chat/pubspec.yaml b/packages/flutter_chat/pubspec.yaml index cdfcede..e6ee2d5 100644 --- a/packages/flutter_chat/pubspec.yaml +++ b/packages/flutter_chat/pubspec.yaml @@ -1,68 +1,37 @@ name: flutter_chat description: "A new Flutter package project." version: 0.0.1 -homepage: +homepage: https://www.iconica.app publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub + environment: - sdk: '>=3.4.3 <4.0.0' + sdk: ">=3.4.3 <4.0.0" flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - + cached_network_image: ^3.2.2 intl: any - + flutter_image_picker: git: url: https://github.com/Iconica-Development/flutter_image_picker ref: 1.0.5 flutter_profile: git: - ref: 1.5.0 + ref: 1.6.0 url: https://github.com/Iconica-Development/flutter_profile - chat_repository_interface: + chat_repository_interface: path: ../chat_repository_interface dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages